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

## 概要

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には１）PSKのみを利用するモードと２）PSKとkey_share拡張に含まれるパラメータを使って再度DH鍵交換を行うモードがあります。0-RTT(Early Data) では、さらに`Client Hello`にEarly Data 拡張を含めることで最初のハンドシェークメッセージ送信から暗号化したデータを送信します。

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


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

### 1. wolfSSL のソースコード取得とビルド
最新コードを github から取得します。

```sh
$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`オプションは技術評価などのためのものです。運用システムには組み込まないように注意してください。

```sh
$ ./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 関数定義が出てくる付近に下記関数を追加します。

```c
#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コンテキスト作成直後に追加します。

```c
    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 を動かしてみる

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

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

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

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

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


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

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

```text
セッションチケットからの鍵の導出
psk = HKDF-Expand-Label(resumption_master_secret,
                        "resumption", ticket_nonce, Hash.length)
```

```text
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683403/df24f938-08bc-12d0-9d07-858161c689c2.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683403/deb9a6e3-2439-508a-786d-843dc744d1bd.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683403/a42bb486-2ac2-233c-af60-2e6d142c77b7.png)

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

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

 a. まず`HKDF-Expand-Label`で`resumption_master_secret`から導出される`PSK`を導出します。

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

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

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

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

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

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

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


```c
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`の値は下記図のような値でした。最初の１バイト、0x20は長さを表します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683403/7658bd83-e024-05c9-73f2-ca87d5a54712.png)

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

```sh
$ ./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_data`TLS拡張をクライアントへ送信しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683403/a5fac266-3d8d-0f40-4685-77aa72ef9c06.png)

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683403/c48660f5-7886-50b1-fbb8-934ee132119f.png)

## まとめ

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

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