概要
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
で出力するファイル名を指定します。
キーログファイルの力を借り、最初のセッションを覗くと下記の図のようになります。
サーバーから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
)を覗いてみます。
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
を導出します。
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_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()
に追加しサーバ/クライアントプログラムを実行します。
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は長さを表します。
サーバプログラムの出力も確認すると同じ値となっていることが確認できました。
$ ./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拡張をクライアントへ送信しています。
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拡張をサーバーへ送信します。
まとめ
今回は、組み込み向けTLSライブラリを使って0-RTTの動きの詳細を追ってみました。Early Dataは、前方秘匿性を犠牲にしている点、またリプライ攻撃に対して防衛手段がない点に注意が必要です。とは言ってもネットワークのラウンドトリップを減らすことでパフォーマンス向上のつながることは、QUICも0-RTTをサポートするモチベーションの一つです。次回はQUICの0-RTTを追ってみたいと思います。
この記事の内容を含めTLSに関する解説をまとめた本を出版する機会を頂きました。セッション再開やEarly Dataの非常に簡単なサンプルプログラムも含まれています。興味があるかたは是非ご覧ください。徹底解部 TLS 1.3