LoginSignup
0
2

More than 1 year has passed since last update.

TLS1.3 の 0-RTT(Early Data) を解剖してみる

Last updated at Posted at 2023-02-07

概要

TLS の最新バージョン 1.3 では性能面での改善項目として 0-RTT (Early Data) が導入されました。これまでのTLS/SSLの性能上の悩みの種は冒頭のハンドシェイクによるオーバヘッドです。これを解消するために、あえて前方秘匿性の保証を外し導入されたのがTLS 1.3 の 0-RTT (Early Data) です。0-RTTとは、TLSハンドシェークの0往復目からデータを暗号化し対向(サーバー)へ送信することです。今回は TLS 1.3 の 0-RTT (Ealry Data) の動作を組み込み向けTLSライブラリを利用しその詳細を確認します。そして次回は新しい通信プロトコルとして注目を集める QUIC の 0-RTT についてもその動作を見てみることにします。

TLS 1.3 では、TLS 1.2 までのセッションIDやセッションチケットが整理され再開時のプロトコルはPSK(Pre Shared Key)プロトコルだけになりました。サーバーは New Session Ticket メッセージでPSKを安全なセッション確立後のポストハンドシェークメッセージの1つとしてクライアントへ送信します。次回のセッション再開時にクライアントは送られてきたセッションチケットを利用します。TLS1.3のPSKには1)PSKのみを利用するモードと2)PSKとkey_share拡張に含まれるパラメータを使って再度DH鍵交換を行うモードがあります。0-RTT(Early Data) では、さらにClient HelloにEarly Data 拡張を含めることで最初のハンドシェークメッセージ送信から暗号化したデータを送信します。

この記事では、TLSライブラリ wolfSSL を使って、実際にプロトコルの動きを見ながら 0-RTT の詳細を追っていくことにします。

wolfSSLのダウンロード、ビルド、インストール

1. wolfSSL のソースコード取得とビルド

最新コードを github から取得します。

$git clone --depth 1 https://github.com/wolfSSL/wolfssl 

--enable-session-ticket --enable-earlydataオプションを指定し wolfSSL にearly data 及び Session Ticket機能を組み込みます。また、のちほど WireShark でパケット復号のために、--enable-opensslextra及びHAVE_SECRET_CALLBACK定義も指定しておきます。HAVE_SECRET_CALLBACKオプションは技術評価などのためのものです。運用システムには組み込まないように注意してください。

$ ./autogen.sh
$ ./configure --enable-session-ticket --enable-earlydata --enable-opensslextra CFLAGS="-DHAVE_SECRET_CALLBACK"
$ make

2. example/client.c に少し修正を加える

WireShark でパケットを復号するための鍵をファイルへ書き出す変更を行います。<wolfSSL Root Folder>/exmaples/client/client.c の先頭付近、ヘッダーや冒頭の変数宣言後、最初の static 関数定義が出てくる付近に下記関数を追加します。

#define SSLKEYLOGFILE "MyKeyLog.txt"
#if defined(SSLKEYLOGFILE)
static void MyKeyLog_cb(const SSL* ssl, const char* line)
{
    FILE* fp;
    const byte  lf = '\n';
    (void)ssl;

    fp = fopen(SSLKEYLOGFILE, "a");
    fwrite(line, 1, strlen(line), fp);
    fwrite((void*)&lf, 1, 1, fp);
    fclose(fp);
}
#endif /* SSLKEYLOGFILE */

定義した関数をライブラリからコールバックするようにwolfSSL_CTX_set_keylog_callbac()呼び出しをwolfSSLコンテキスト作成直後に追加します。

    if (method != NULL) {
        ctx = wolfSSL_CTX_new(method(NULL));
        if (ctx == NULL)
            err_sys("unable to get ctx");
    }
#endif
wolfSSL_CTX_set_keylog_callback(ctx, MyKeyLog_cb);//追加コード

再度、makeしクライアントプログラムがコンパイルできることを確認します。

サンプルクライアント/サーバーで 0RTT を動かしてみる

サンプルサーバーを立ち上げます

$./examples/server/server -v 4 -0 -i

-v 4は、TLS1.3を使うことを明示的に指定します。-0は、early dataを使用することを、-iは繰り返し接続を許可することを指定します。

別のターミナルを起動し、サンプルクライアントを起動します。

$./examples/client/client -v 4 -0 -r

-v 4は、TLS1.3を使うことを明示的に指定します。-0は、early dataを使用することを、-rはセッション再開を行うことを指示しています。このオプションを指定すると、一つのコマンドの中で、最初のセッションとセッション再開と2回の接続を行います。

PSK とフルハンドシェイクの鍵導出スケジュール

Wiresharkでパケットを覗いてみる前にTLS1.3のセッションチケットとフルハンドシェイクにおける鍵の導出を確認しておきます。それぞれ、 RFC8446で次のように規定されています。HKDFは、一方向性の性質をもつハッシュによる鍵導出関数です。ハンドシェイクの中でどのようにスケジュールが使われるかは、この後実際のパケットを見ながら説明します。

セッションチケットからの鍵の導出
psk = HKDF-Expand-Label(resumption_master_secret,
                        "resumption", ticket_nonce, Hash.length)
PSKとフルハンドシェイクの鍵導出スケジュール
             0
             |
             v
   PSK ->  HKDF-Extract = Early Secret
             |
             +-----> Derive-Secret(., "ext binder" | "res binder", "")
             |                     = binder_key
             |
             +-----> Derive-Secret(., "c e traffic", ClientHello)
             |                     = client_early_traffic_secret
             |
             +-----> Derive-Secret(., "e exp master", ClientHello)
             |                     = early_exporter_master_secret
             v
       Derive-Secret(., "derived", "")
             |
             v
   (EC)DHE -> HKDF-Extract = Handshake Secret
             |
             +-----> Derive-Secret(., "c hs traffic",
             |                     ClientHello...ServerHello)
             |                     = client_handshake_traffic_secret
             |
             +-----> Derive-Secret(., "s hs traffic",
             |                     ClientHello...ServerHello)
             |                     = server_handshake_traffic_secret
             v
       Derive-Secret(., "derived", "")
             |
             v
   0 -> HKDF-Extract = Master Secret
             |
             +-----> Derive-Secret(., "c ap traffic",
             |                     ClientHello...server Finished)
             |                     = client_application_traffic_secret_0
             |
             +-----> Derive-Secret(., "s ap traffic",
             |                     ClientHello...server Finished)
             |                     = server_application_traffic_secret_0
             |
             +-----> Derive-Secret(., "exp master",
             |                     ClientHello...server Finished)
             |                     = exporter_master_secret
             |
             +-----> Derive-Secret(., "res master",
                                   ClientHello...client Finished)
                                   = resumption_master_secret

WireShark でパケットを覗いてみる

WireShark を起動しearly dataの中身を覗いてみます。Wiresharkの起動後、復号化のためにキーログファイルの指定します。設定は、メニューバーから「編集」→「設定」→「Protocols」→「TLS」を選択するとTransport Layer Security の設定画面になります。この画面の一番下に「(Pre)-Master-Secret log filename」設定欄があるので、「Browse...」ボタンを押して、前述の修正したclient.cで出力するファイル名を指定します。

image.png

キーログファイルの力を借り、最初のセッションを覗くと下記の図のようになります。

image.png

サーバーからNew Session Ticketメッセージが送出されています。New Session Ticketにはearly_data拡張が含まれており、サーバーがearly_dataの受信が可能であることを示しています。New Session Ticketには他に下記のようなものが含まれます。

属性 備考
lifetime チケットの寿命を秒単位で示す。
ticket_age_add チケット経過時間を分かり難くするための値。外部からの影響を難しくするためにサーバーから送られた値を追加し外部から実際の経過時間の判別を困難にする
ticket_nonce 0~255の値。セッション毎に発行したチケット毎に一意の値。
ticket PSK IDとして使用されるチケットの値
extensions 拡張early_dataのみが対象。0-RTTデータの送信が可能であることを示す。

次にセッション再開時のClient Hello(WireShark上では2回目のClient Hello)を覗いてみます。

image.png

pre_shared_key拡張が含まれています。pre_shared_key拡張にはPSK IdentityPSK Bindersという属性が含まれています。
PSK IdentityIdentityには、先ほどサーバーがNew Session Ticketで送ってきた値がそのまま入っています。Obfuscated Ticketにはticket_age_addの値が入っています。

PSK Binders値は、PSKの認証のために使用します。PSK Bindersの値は次のように導き出されます。

a. まずHKDF-Expand-Labelresumption_master_secretから導出されるPSKを導出します。

psk = HKDF-Expand-Label(resumption_master_secret,
                        "resumption", ticket_nonce, Hash.length)

b. 次に鍵導出スケジュールの下記で示されるbinder_keyを導出します。

入力文字列は、セッション再開の場合、res binderとなります。

   PSK ->  HKDF-Extract = Early Secret
             |
             +-----> Derive-Secret(., "ext binder" | "res binder", "")
             |                     = binder_key

c. 求めたbinder_keyPSK Binders直前までのClient Helloメッセージのハッシュ値を使ってFinishedメッセージと同じようにHMAC値を算出します。

次にサーバー側でも同じことを行っていて、サーバーとクライアントで同じHMAC値(PSK Binders)となっているかを検証します。

これは、wolfSSLのソースコード中で src/tls13.cDoPreSharedKeys() という関数で行われています。試しにサーバで算出したHMAC値を出力するようにし、クライアントが送ってきたものと同じ値になっているか確認してみます。そのためにソースコードに少し修正を加えてサーバ算出値を出力します。下記の/* test code to dump server side binder value */行以下、8行分をDoPreSharedKeys()に追加しサーバ/クライアントプログラムを実行します。

tls13.c
/* Handle any Pre-Shared Key (PSK) extension.
 * Find a PSK that supports the cipher suite passed in.
 *
 * ssl         SSL/TLS object.
 * suite       Cipher suite to find PSK for.
 * usingPSK    1=Indicates handshake is using Pre-Shared Keys (2=Ephemeral)
 * first       Set to 1 if first in extension
 * returns 0 on success and otherwise failure.
 */
static int DoPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 inputSz,
    byte* suite, int* usingPSK, int* first)
{
...
        /* Derive the binder and compare with the one in the extension. */
        ret = BuildTls13HandshakeHmac(ssl,
                         ssl->keys.client_write_MAC_secret, binder, &binderLen);
        if (ret != 0)
            return ret;
        /* test code to dump server side binder value */
        {
            byte i;
            printf("Len %d Binder:\n", binderLen);
            for (i=0; i < binderLen; i++) {
                printf("%02x", binder[i]);
            }
            printf("\n");
        }
        if (binderLen != current->binderLen ||
                             XMEMCMP(binder, current->binder, binderLen) != 0) {
            WOLFSSL_ERROR_VERBOSE(BAD_BINDER);
            return BAD_BINDER;
        }
...
}

先ほどと同じように Wireshark でキャプチャします。 この時の Client Hello の PSK Bindersの値は下記図のような値でした。最初の1バイト、0x20は長さを表します。
image.png

サーバプログラムの出力も確認すると同じ値となっていることが確認できました。

$ ./examples/server/server -v 4 -0 -i
Early Data was not sent.
Alternate cert chain used
 issuer : /C=US/ST=Montana/L=Bozeman/O=wolfSSL_2048/OU=Programming-2048/CN=www.wolfssl.com/emailAddress=info@wolfssl.com
 subject: /C=US/ST=Montana/L=Bozeman/O=wolfSSL_2048/OU=Programming-2048/CN=www.wolfssl.com/emailAddress=info@wolfssl.com
 altname = example.com
 serial number:73:fb:54:d6:03:7d:4c:07:84:e2:00:11:8c:dd:90:dc:48:8d:ea:53 
SSL version is TLSv1.3
SSL cipher suite is TLS_AES_128_GCM_SHA256
SSL signature algorithm is (null)
SSL curve name is SECP256R1
Server Random : DB931B41DF1C6D3A13E30E5DE9D25194711FD227F91E2D53B0BB06F695371E46
Client message: hello wolfssl!
Len 32 Binder:
8962fdbed45060fb1332ebfc99ace610f40d82d60c11ffb6b64bac32aac0a53d // <---サーバの算出値
Early Data was accepted
peer has no cert!
SSL version is TLSv1.3
SSL cipher suite is TLS_AES_128_GCM_SHA256
SSL curve name is SECP256R1
SSL reused session
Server Random : 60D37EDB0CCED7F070C53EE2530C657607D8B49846C0B0259C490605BBBFFB59
Client message: resuming wolfssl!

続いてパケットキャプチャの続きを見ると、検証に成功したサーバーはearly_dataを処理するつもりであることを示すためにearly_dataTLS拡張をクライアントへ送信しています。

image.png

0-RTTの際にクライアントはclient_early_traffic_secretを使ってEarly Dataを暗号化します。Early Dataを送信することを事前にサーバーへ通知するためにClient Helloearly_data拡張を含めます。early_dataは前回の鍵情報(resumption_master_secret)を使用し算出します。client_early_traffic_secretを使って暗号化するため前方秘匿性が損なわれます。ただし、その後のApplication Data は、前述の2) PSK と DH鍵交換を行うモードの場合、再度、鍵を算出するため前方秘匿性は保たれます。

クライアントはearly data送信後、データの終わりを示すEnd of Early DataTLS拡張をサーバーへ送信します。

image.png

まとめ

今回は、組み込み向けTLSライブラリを使って0-RTTの動きの詳細を追ってみました。Early Dataは、前方秘匿性を犠牲にしている点、またリプライ攻撃に対して防衛手段がない点に注意が必要です。とは言ってもネットワークのラウンドトリップを減らすことでパフォーマンス向上のつながることは、QUICも0-RTTをサポートするモチベーションの一つです。次回はQUICの0-RTTを追ってみたいと思います。

この記事の内容を含めTLSに関する解説をまとめた本を出版する機会を頂きました。セッション再開やEarly Dataの非常に簡単なサンプルプログラムも含まれています。興味があるかたは是非ご覧ください。徹底解部 TLS 1.3

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2