QUICのInitialパケットの暗号化キーの生成などのメモ

RFC 9001 Using TLS to Secure QUIC のInitialパケットの暗号化キーの生成などに関するメモです。

Initial Packetに対して、

  • ペイロード(QUIC ヘッダーを除く部分) の保護
  • QUICヘッダーの保護

の2種類が適用されます。

Initial パケットは、クライアントが最初に送信するInitialパケットに含まれるDestination Connection IDから導出されたキー用いてAEAD_AES_128_GCM を使います。

Initial シークレットとキーの生成には、SHA-256が使用されます。

全体の流れ

クライアントとサーバーが共通で使うInitial シークレットを生成します。

Initialシークレットから、クライアントとサーバーそれぞれがシークレットを生成します。

そのシークレットをもとに、AEADに使用するキー、AEADのナンスの生成に使用するInitial Vector (IV)、ヘッダ保護用のキーの生成を行います。

Initial シークレットの生成

Initialシークレットは、 HKDF-Extractという処理で生成します。

HKDF-Extractは、RFC5869 HMAC-based Extract-and-Expand Key Derivation Function (HKDF) で定義されています。

HKDF-Extract はソルトとIKM(input keying material)を入力として受け取り、PRK(pseudorandom key)を出力します。オプションとして、ハッシュ関数を指定することができます。

HKDF-Extractの入力

QUICのInitialパケットの場合、ソルトには、0x38762cf7f55934b34d179ae6a4c80cadccbb7f0a という値を使います。IKMには、Destination Connection IDを使用します。

ちなみに0x38762cf7f55934b34d179ae6a4c80cadccbb7f0a という値はSHA-1が初めて衝突した時のハッシュ値らしいです。(参考: 巷で話題のGoogleのSHA-1衝突やってみた

HKDF-Expand-Label関数

クライアントが使用するシークレットはHKDF-Extractの出力のPRKと、"client-in" というラベルを入力としてHKDF-Expand-Label 関数を使用し32バイトのシークレットを生成します。

TLS1.3 では、以下のようにHKDF-Expand-Labelを定義しています。

       HKDF-Expand-Label(Secret, Label, Context, Length) =
            HKDF-Expand(Secret, HkdfLabel, Length)

HkdfLabelは以下のように定義されます。

       struct {
           uint16 length = Length;
           opaque label<7..255> = "tls13 " + Label;
           opaque context<0..255> = Context;
       } HkdfLabel;

実際のラベルのデータ

QUICのクライアントのInitialシークレットを生成する場合は、"tls13 client in" というラベルを使用します。また、Contextは空の文字列を使用します。

定義より、opque label<7..255> は、7から255バイトの可変長のベクターです。

実際のデータを構造体に格納する場合は、この変数labelの長さを格納します。この場合は最大の長さ255を表すために必要な1バイトを使用して長さを表します。

同じく、空のcontextであっても、長さが0であることを表す情報は必要になります。

実際には、以下のようなデータが格納されます。

  • 32を2バイトで表したもの
  • "tls13 client in" の長さを1バイトで表したもの
  • "tls13 client in" の文字列
  • contextの長さ0を1バイトで表したもの

シークレットを用いたクライアント用のキー生成

クライアント用のシークレットとラベル"quic key" をHKDF-Expand-Labelの入力として、AEADのキーを生成します。

AEADとは、The Authenticated Encryption with Associated Data (AEAD) 関数 AEAD の略で、パケットの保護に使われます。

AEADは入力として、シークレットキーK、ナンスN、プレインテキストP、関連データ (associated data) A を入力とし、暗号文Cを生成します。

シークレットと"quic iv" をHKDF-Expand-Label入力とし、パケット番号とあわせてナンスの生成に使用するInitial Vector (IV) を 生成します。

また、ヘッダープロテクションに使うキーは、ラベル "quic hp" を入力として生成します。

サーバー側の場合は、"client in" の代わりに "server in" を使います。それ以降の流れは同じです。

実際に生成してみる

RFC9001のAnnexAをもとに、実際に生成してみます。言語はpython、hkdfの処理には、python-hkdf というライブラリを使用しました。

RFCの例では、 Destination Connection ID に 0x8394c8f03e515708 を使用しています。

Initialシークレットの生成

まずは、intialシークレット (PRK) を生成します。ソルトとCIDをHKDFの入力にします。また、ハッシュ関数はsha256を指定します。

import hkdf
import hashlib
from binascii import unhexlify

salt = unhexlify('38762cf7f55934b34d179ae6a4c80cadccbb7f0a')
cid  = unhexlify('8394c8f03e515708')
initial_secret = hkdf.hkdf_extract(salt = salt, input_key_material = cid, hash=hashlib.sha256)
print("initial_secret         : {}".format(initial_secret.hex()))
print("expected initial_secret: 7db5df06e7a69e432496adedb00851923595221596ae2ae9fb8115c1e9ed0a44")

これを実行すると、Annex A.1に記載のInitialシークレットと同じものが生成できます。

initial_secret         : 7db5df06e7a69e432496adedb00851923595221596ae2ae9fb8115c1e9ed0a44
expected initial_secret: 7db5df06e7a69e432496adedb00851923595221596ae2ae9fb8115c1e9ed0a44

client側のキーの生成

次に、HKDF-Expand-Label 関数を定義します。今回使用したhkdfのライブラリには、HKDF-Expand-Label関数の実装はないので、自前で定義します。

def hkdf_expand(secret, text, length):
    label_text = b"tls13 " + text
    label = (length).to_bytes(2, 'big') + (len(label_text)).to_bytes(1, 'big') + label_text + (0).to_bytes(1, 'big')
    print("label: {}".format(label.hex()))
    return hkdf.hkdf_expand(secret, info = label, length = length, hash = hashlib.sha256)

この関数を使用して、クライアントのInitialシークレット、キー、IV、ヘッダプロテクションキーをそれぞれ生成します。

client_initial_secret = hkdf_expand(secret = initial_secret, text = b"client in", length = 32)
print("client_initial_secret          : {}".format(client_initial_secret.hex()))
print("expected client_initial_secret : c00cf151ca5be075ed0ebfb5c80323c42d6b7db67881289af4008f1f6c357aea")

key = hkdf_expand(secret = client_initial_secret, text=b"quic key", length=16)
print("key         : {}".format(key.hex()))
print("expected key: 1f369613dd76d5467730efcbe3b1a22d")

iv = hkdf_expand(secret = client_initial_secret, text = b"quic iv", length = 12)
print("iv          : {}".format(iv.hex()))
print("expected iv : fa044b2f42a3fd3b46fb255c")

hp = hkdf_expand(secret = client_initial_secret, text = b'quic hp', length = 16)
print("hp          : {}".format(hp.hex()))
print("expected hp : 9f50449e04a0e810283a1e9933adedd2")

出力を見ると、それぞれAnnexA.1に書かれているものと同じ結果が生成されていることが分かります。

また、それぞれ "client in"、"quic key"、"quic iv"、"quic hp" から生成されるラベルもAnnexA.1 に書かれているものと同じことが分かります。

label: 00200f746c73313320636c69656e7420696e00
client_initial_secret          : c00cf151ca5be075ed0ebfb5c80323c42d6b7db67881289af4008f1f6c357aea
expected client_initial_secret : c00cf151ca5be075ed0ebfb5c80323c42d6b7db67881289af4008f1f6c357aea
label: 00100e746c7331332071756963206b657900
key         : 1f369613dd76d5467730efcbe3b1a22d
expected key: 1f369613dd76d5467730efcbe3b1a22d
label: 000c0d746c733133207175696320697600
iv          : fa044b2f42a3fd3b46fb255c
expected iv : fa044b2f42a3fd3b46fb255c
label: 00100d746c733133207175696320687000
hp          : 9f50449e04a0e810283a1e9933adedd2
expected hp : 9f50449e04a0e810283a1e9933adedd2

サーバー側のキー生成

サーバー側も同じようにすると、AnnexA.1 に書かれているものと同じものが生成できます。

server_initial_secret = hkdf_expand(secret = initial_secret, text = b"server in", length = 32)
print("server_initial_secret          : {}".format(server_initial_secret.hex()))
print("expected server_initial_secret : 3c199828fd139efd216c155ad844cc81fb82fa8d7446fa7d78be803acdda951b")

key = hkdf_expand(secret = server_initial_secret, text=b"quic key", length=16)
print("key         : {}".format(key.hex()))
print("expected key: cf3a5331653c364c88f0f379b6067e37")

iv = hkdf_expand(secret = server_initial_secret, text = b"quic iv", length = 12)
print("iv          : {}".format(iv.hex()))
print("expected iv : 0ac1493ca1905853b0bba03e")

hp = hkdf_expand(secret = server_initial_secret, text = b'quic hp', length = 16)
print("hp          : {}".format(hp.hex()))
print("expected hp : c206b8d9b9f0f37644430b490eeaa314")

出力

erver_initial_secret          : 3c199828fd139efd216c155ad844cc81fb82fa8d7446fa7d78be803acdda951b
expected server_initial_secret : 3c199828fd139efd216c155ad844cc81fb82fa8d7446fa7d78be803acdda951b
label: 00100e746c7331332071756963206b657900
key         : cf3a5331653c364c88f0f379b6067e37
expected key: cf3a5331653c364c88f0f379b6067e37
label: 000c0d746c733133207175696320697600
iv          : 0ac1493ca1905853b0bba03e
expected iv : 0ac1493ca1905853b0bba03e
label: 00100d746c733133207175696320687000
hp          : c206b8d9b9f0f37644430b490eeaa314
expected hp : c206b8d9b9f0f37644430b490eeaa314

参考

RFC9001

RFC 9001: Using TLS to Secure QUIC

The Illustrated QUIC Connectionというサイトが、QUICの接続手順を暗号化まで含めて解説しているので大変参考になります。

quic.ulfheim.net