「TLS1.2とTLS1.3のプロトコルについて」で TLS1.2 と TLS1.3 の違いについて書きました。今回は TLS1.3 でどのように暗号化が行われているかを少し詳細に書いてみました。
1. TLS1.3のシーケンス
2. 暗号化の例
2.1. ClientHello(クライアント → サーバ)
ClientHello
random = C_RANDOM
cipher_suites =
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
supported_groups =
X25519, secp256r1
key_share =
group: X25519
public_key: C_pub
signature_algorithms =
rsa_pss_rsae_sha256
ecdsa_secp256r1_sha256
-
random
- クライアントで生成した32バイトの乱数を設定
-
cipher_suites
- クライアントが使用できる暗号スイートの一覧
- TLS_AES_128_GCM_SHA256
- 通信の暗号アルゴリズム → AES_128_GCM
- HKDF(鍵導出関数)で使うハッシュアルゴリズム → SHA256
- TLS_AES_256_GCM_SHA384
- 通信の暗号アルゴリズム → AES_256_GCM
- HKDF(鍵導出関数)で使うハッシュアルゴリズム → SHA384
- TLS_AES_128_GCM_SHA256
- クライアントが使用できる暗号スイートの一覧
-
supported_groups
- 鍵交換で使用するグループ(楕円曲線や有限体)の候補
- X25519, secp256r1
- 楕円曲線ディフィー・ヘルマン(ECDHE)で使う「曲線の種類」
- X25519, secp256r1
- 鍵交換で使用するグループ(楕円曲線や有限体)の候補
TLS 1.2では、鍵交換アルゴリズムとしてRSA, DH, DHE, ECDH, ECDHEが使用できましたが、TLS1.3ではDHE, ECDHEとなっています。
TLS 1.3のフルハンドシェイクでは、前方秘匿性(PFS)を確保するために、(EC)DHE(またはそれとポスト量子暗号を組み合わせたハイブリッド方式)による鍵共有が必須となっています。
-
key_share
- 鍵交換で使用するグループ(楕円曲線や有限体)とその公開鍵
- group → 鍵交換で使用するグループを指定
- public_key → クライアントの公開鍵を指定
- 鍵交換で使用するグループ(楕円曲線や有限体)とその公開鍵
サーバがクライアントの最初のkey_shareグループに対応していなかった場合のみ、サーバは HelloRetryRequest を返します。
(例: X25519 未対応だが secp256r1 は対応している場合)
HelloRetryRequest
group: secp256r1
HelloRetryRequestを受け取ったクライアントはClientHelloを再送します。
ClientHello(再送)
key_share =
group: secp256r1
public_key: 新しい C_pub
-
signature_algorithms
- サーバが署名で使ってよいアルゴリズム一覧
- rsa_pss_rsae_sha256
- rsa_pss:署名方式(RSA-PSS署名方式)
- rsae:鍵の種類(RSA公開鍵)
- sha256:ハッシュアルゴリズム
- ecdsa_secp256r1_sha256
- ecdsa:署名方式(楕円曲線署名)
- secp256r1:鍵の種類(使用する曲線)
- sha256:ハッシュアルゴリズム
- rsa_pss_rsae_sha256
- サーバが署名で使ってよいアルゴリズム一覧
この時点でクライアントとサーバは初期シークレット(early_secret)を生成します。
TLS1.3では、PSK(Pre-Shared Key) がない場合は0で初期値を設定します。
early_secret = HKDF-Extract(0, PSK=0)
PRK = HKDF-Extract(salt, input_key_material)
salt : 前段のシークレット(鍵導出の基準となる値)
input : 新しく取り込むシークレット
2.2. ServerHello(サーバ → クライアント)
ServerHello
random = S_RANDOM
cipher_suite =
TLS_AES_128_GCM_SHA256
key_share =
group: X25519
public_key: S_pub
-
random
- サーバで生成した32バイトの乱数を設定
-
cipher_suite
- 使用する暗号スイートを決定
- クライアントに提示された候補から選択し
TLS_AES_128_GCM_SHA256に決定
- クライアントに提示された候補から選択し
- 使用する暗号スイートを決定
-
key_share
- 鍵交換で使用するグループ(楕円曲線や有限体)とその公開鍵
- group → 鍵交換で使用するグループを指定
- public_key → サーバの公開鍵を指定
- 鍵交換で使用するグループ(楕円曲線や有限体)とその公開鍵
TLS1.3 では HelloRetryRequest は ServerHello と同じ構造を持つ特殊メッセージです。
サーバが対応可能なグループを通知し、クライアントはそのグループで ClientHello を再送します。
例:
HelloRetryRequest
group: secp256r1
ClientHello(再送)
key_share =
group: secp256r1
public_key: 新しい C_pub
この時点で、暗号スイートと鍵交換で使用するグループ及びその公開鍵が決まります。
- 暗号スイート : AES_128_GCM + SHA256
- 鍵交換 : X25519
- 公開鍵 : C_pub、S_pub
ここで、クライアントとサーバそれぞれで、
- 共有鍵(shared_secret)
- ハンドシェイクシークレット(handshake_secret)
- ハンドシェイク鍵(client_handshake_traffic_secret、server_handshake_traffic_secret)
を生成します。
2.2.1. 共有鍵(shared_secret)の生成
クライアント
shared_secret = X25519(C_priv, S_pub)
- C_priv : クライアントの秘密鍵
- S_pub : サーバの公開鍵
サーバ
shared_secret = X25519(S_priv, C_pub)
- S_priv : サーバの秘密鍵
- C_pub : クライアントの公開鍵
2.2.2. ハンドシェイクシークレット(handshake_secret)の生成
クライアント及びサーバ
handshake_secret = HKDF-Extract(early_secret, shared_secret)
- early_secret : 初期シークレット
- shared_secret : 共有鍵
2.2.3. ハンドシェイク鍵(client_handshake_traffic_secret、server_handshake_traffic_secret)の生成
クライアント及びサーバ
client_handshake_traffic_secret = HKDF-Expand(handshake_secret, "c hs traffic", L)
- handshake_secret : ハンドシェイクシークレット
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
server_handshake_traffic_secret = HKDF-Expand(handshake_secret, "s hs traffic", L)
- handshake_secret : ハンドシェイクシークレット
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
OKM = HKDF-Expand(PRK, info, L)
- OKM : 出力鍵材料
- PRK : 前段のシークレット(鍵導出の基準となる値)
- info : 鍵の用途やラベルなどの追加情報(導出する鍵を区別するため)
- L : 生成したい鍵の長さ(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
2.2.4. ハンドシェイク用の AES鍵と IV の生成
クライアント及びサーバ
client_handshake_write_key = HKDF-Expand(client_handshake_traffic_secret, "key", L)
client_handshake_write_IV = HKDF-Expand(client_handshake_traffic_secret, "iv", 12)
server_handshake_write_key = HKDF-Expand(server_handshake_traffic_secret, "key", L)
server_handshake_write_IV = HKDF-Expand(server_handshake_traffic_secret, "iv", 12)
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
- client_handshake_write_key : クライアントが「送信データを暗号化」、サーバが「受信データを復号」するために使用
- client_handshake_write_IV : クライアントが送信データ暗号化で AES-GCM の初期化ベクトルとして使用、サーバは受信データ復号で同じ IV を使用
- server_handshake_write_key : サーバが「送信データを暗号化」、クライアントが「受信データを復号」するために使用
- server_handshake_write_IV : サーバが送信データ暗号化で AES-GCM の初期化ベクトルとして使用、クライアントは受信データ復号で同じ IV を使用
ハンドシェイク用の AES鍵と IV は EncryptedExtensions, Certificate, CertificateVerify, Finished の暗号化/復号に使用されます。
2.3. EncryptedExtensions(サーバ → クライアント)
EncryptedExtensions
ALPN: http/1.1
- 暗号化
- 鍵 → server_handshake_traffic_secret
- アルゴリズム → AES_128_GCM
2.4. Certificate(サーバ → クライアント)
Certificate
サーバ証明書(RSA公開鍵)
クライアントはCA公開鍵でサーバ証明書の署名検証を行い、サーバ証明書が本物であることを検証します。
※サーバ証明書の確認については「TLSにおけるブラウザの証明書のチェック」を参照
2.5. CertificateVerify(サーバ → クライアント)
CertificateVerify
algorithm = ecdsa_secp256r1_sha256
signature = Sign(handshake_messages, サーバ秘密鍵)
- algorithm : 署名アルゴリズム(ClientHello の signature_algorithms の候補からサーバが選択)
- handshake_messages : ハンドシェイク中に送受信したメッセージをすべて連結したもの
- signature : handshake_messages のハッシュ値(選択したハッシュアルゴリズム「ここでは
ecdsa_secp256r1_sha256」を使用)をサーバ秘密鍵で署名
クライアントで署名を検証します。
検証がOKであれば、サーバが秘密鍵を持っていることが証明されます。
Verify(signature, サーバ公開鍵)
- handshake_messages のハッシュ値(サーバが指定したハッシュアルゴリズム「ここでは
ecdsa_secp256r1_sha256」を使用)を求める - signature をサーバの公開鍵で検証(handshake_messages のハッシュ値の一致を確認)
2.6. Finished(サーバ → クライアント)
Finished
verify_data
verify_data は以下で求めます。
verify_data =
HMAC(
finished_key,
Hash(handshake_messages)
)
- finished_key : HMAC の key(HKDF-Expand(handshake_secret, "finished", L) で導出)
- Hash(handshake_messages) : HMAC の message(ハンドシェイクでやり取りした全メッセージのハッシュ)
なお、finished_key は以下で生成します。
finished_key =
HKDF-Expand(server_handshake_traffic_secret, "finished", L)
- server_handshake_traffic_secret : ハンドシェイクシークレット
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
出力 = HMAC(key, message)
- key : 認証のための秘密鍵(固定長のバイト列)
- message : 認証対象のデータ
- 出力 : 認証コード(MAC値、改ざん検出用)
HMAC(Hash-based Message Authentication Code)は ハッシュ関数を用いたメッセージ認証コード です。
クライアント側では同じ計算をして一致確認します。
finished_key = HKDF-Expand(server_handshake_traffic_secret, "finished", L)
- server_handshake_traffic_secret : ハンドシェイクシークレット(server_handshake_traffic_secretを使用する)
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
verify_data_local = HMAC(finished_key, Hash(handshake_messages))
- finished_key : HMAC の key(HKDF-Expand(server_handshake_traffic_secret, "finished", L) で導出)
- Hash(handshake_messages) : HMAC の message(ハンドシェイクでやり取りした全メッセージのハッシュ)
verify_data と verify_data_local が一致することを確認します。
2.7. Finished(クライアント → サーバ)
Finished
verify_data = verify_data_client
verify_data_client は以下で求めます。
verify_data_client =
HMAC(
finished_key,
Hash(handshake_messages)
)
- finished_key : HMAC の key(HKDF-Expand(handshake_secret, "finished", L) で導出)
- Hash(handshake_messages) : HMAC の message(ハンドシェイクでやり取りした全メッセージのハッシュ)
なお、finished_key は以下で生成します(AES-GCM 暗号化とは独立に HMAC 用の鍵として導出されます)。
finished_key =
HKDF-Expand(client_handshake_traffic_secret, "finished", L)
- client_handshake_traffic_secret : ハンドシェイクシークレット
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
サーバ側では同じ計算をして一致確認します。
finished_key = HKDF-Expand(client_handshake_traffic_secret, "finished", L)
- client_handshake_traffic_secret : ハンドシェイクシークレット(client_handshake_traffic_secret を使用する)
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
verify_data_server = HMAC(finished_key, Hash(handshake_messages))
- finished_key : HMAC の key(HKDF-Expand(client_handshake_traffic_secret, "finished", L) で導出)
- Hash(handshake_messages) : HMAC の message(ハンドシェイクでやり取りした全メッセージのハッシュ)
verify_data(クライアント) と verify_data_server が一致することを確認します。
アプリケーション鍵の生成
クライアント及びサーバでアプリケーション鍵を生成します。
master_secret生成
master_secret = HKDF-ExpandLabel(handshake_secret, "master secret", "", L)
- handshake_secret : ハンドシェイクシークレット
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
OKM = HKDF-ExpandLabel(Secret, Label, Context, L)
- Secret : 元のシークレット(PRK に相当)
- Label : 用途を示す文字列(例: "master secret", "c ap traffic", "key", "iv")
- Context: 任意の追加情報(通常は空文字列)
- L : 生成する鍵の長さ(バイト単位)
アプリ鍵生成
client_application_traffic_secret = HKDF-Expand(master_secret, "c ap traffic", L)
server_application_traffic_secret = HKDF-Expand(master_secret, "s ap traffic", L)
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
AES鍵とIV(初期ベクトル)の生成
client_write_key = HKDF-Expand(client_application_traffic_secret, "key", L)
client_write_IV = HKDF-Expand(client_application_traffic_secret, "iv", 12)
server_write_key = HKDF-Expand(server_application_traffic_secret, "key", L)
server_write_IV = HKDF-Expand(server_application_traffic_secret, "iv", 12)
- L : 暗号アルゴリズムに依存する鍵長(バイト単位)
- AES-128 の場合 → 16
- AES-256 の場合 → 32
- client_write_key : クライアントが「送信データを暗号化」、サーバが「受信データを復号」するために使用
- client_write_IV : クライアントが送信データ暗号化で AES-GCM の初期化ベクトルとして使用、サーバは受信データ復号で同じ IV を使用
- server_write_key : サーバが「送信データを暗号化」、クライアントが「受信データを復号」するために使用
- server_write_IV : サーバが送信データ暗号化で AES-GCM の初期化ベクトルとして使用、クライアントは受信データ復号で同じ IV を使用
2.8. アプリケーション通信
2.8.1. クライアントからの送信例
平文
GET / HTTP/1.1
Host: example.com
暗号化
- クライアントは TLS レコードを作成
- タイプ: Application Data
- バージョン: TLS 1.3
- ペイロード: 上記 HTTP メッセージ
AES-GCM で暗号化
nonce = client_write_IV XOR sequence_number
ciphertext = AES-128-GCM-Encrypt(client_write_key, nonce, plaintext, AAD)
- client_write_IV : 初期化ベクトル(方向ごとに異なる)
- sequence_number : TLS レコードの順序番号
- client_write_key : クライアント送信鍵
- plaintext : 平文
- AAD : TLS レコードヘッダの認証用データ
- 出力は 暗号文 + 認証タグ
TLS1.3 では AES-GCM の認証タグで改ざん検知が可能なため、別途 HMAC は不要です。
暗号化済みデータを TCP に載せて送信
2.8.2. サーバ側での復号
TCP 受信
TLS レコードからペイロードを取り出す
AES-GCM で復号
nonce = client_write_IV XOR sequence_number
plaintext = AES-128-GCM-Decrypt(client_write_key, nonce, ciphertext, AAD)
- client_write_key : クライアントが「送信データを暗号化」、サーバが「受信データを復号」するために使用
- client_write_IV : クライアントが送信データ暗号化で AES-GCM の初期化ベクトルとして使用、サーバは受信データ復号で同じ IV を使用
- sequence_number : TLS レコードの順序番号
- ciphertext : 暗号文 + 認証タグ
- AAD : TLS レコードヘッダの認証用データ
- 出力は 平文
3. 全体の「暗号の役割」まとめ
| フェーズ | 使用暗号 | 役割 |
|---|---|---|
| ClientHello | なし | 候補提示、ハンドシェイク開始 |
| ServerHello | X25519 | 鍵交換(ECDHE) |
| 鍵生成 (handshake_secret → handshake traffic keys) | HKDF(SHA256) | ハンドシェイク鍵導出 |
| EncryptedExtensions | AES_128_GCM | 暗号化ハンドシェイク(サーバ→クライアント) |
| Certificate | AES_128_GCM | 暗号化ハンドシェイク内で送信、身元証明 |
| CertificateVerify | AES_128_GCM | 暗号化ハンドシェイク内で署名検証、なりすまし防止 |
| Finished | AES_128_GCM(認証付き) | 暗号化ハンドシェイク、完全性確認(HMAC verify_data による改ざん検知) |
| Application Data | AES_128_GCM | 暗号化通信(送受信データの機密性と改ざん検知) |
参考
以上