TLS 1.3
TLS 1.3(RFC 8446)はトランスポート層の暗号化における現行標準です。ワイヤフォーマットは複数のフレーミング層が入れ子になっています。TLS レコードがハンドシェイクメッセージを包み、ハンドシェイクメッセージがさらに型付き拡張を含む、という構造です。各層がそれぞれ独自のディスパッチテーブルを持つ TLV(Type-Length-Value)構造です。
このページでは TLS 1.3 の wirespec モデル全体を示し、マッチタグとしての enum 型、u24 プリミティブ、ネストしたカプセル、[T; fill] within EXPR(バイト長区切りの fill 配列)を解説します。
プロトコルの背景
TLS 接続はハンドシェイクから始まり、クライアントとサーバーが暗号スイートの交渉、鍵交換、相互認証を行います。レコード層がすべてのハンドシェイク(およびアプリケーション)トラフィックを 5 バイトのレコードヘッダで包みます。ハンドシェイクメッセージは独自の 4 バイトヘッダ(1 バイト型 + 3 バイト長)を持ち、ハンドシェイクメッセージ内の拡張には 2+2 バイトの TLV ヘッダが使われます。
このレイヤー構造(Record -> HandshakeMessage -> Extensions)が、wirespec のネストしたカプセル機能を実践する良い題材になります。
wirespec 定義
module tls.tls13
@endian big
# -- 列挙型 --
enum ContentType: u8 {
ChangeCipherSpec = 20,
Alert = 21,
Handshake = 22,
ApplicationData = 23,
}
enum HandshakeType: u8 {
ClientHello = 1,
ServerHello = 2,
NewSessionTicket = 4,
EncryptedExtensions = 8,
Certificate = 11,
CertificateRequest = 13,
CertificateVerify = 15,
Finished = 20,
KeyUpdate = 24,
}
# -- Extension (TLV) --
capsule Extension {
extension_type: u16,
length: u16,
payload: match extension_type within length {
0x002b => SupportedVersions { data: bytes[remaining] },
0x000d => SignatureAlgorithms { data: bytes[remaining] },
0x0033 => KeyShare { data: bytes[remaining] },
0x0000 => ServerName { data: bytes[remaining] },
_ => Unknown { data: bytes[remaining] },
},
}
# -- ハンドシェイクパケット --
packet ClientHello {
legacy_version: u16,
random: bytes[32],
session_id_length: u8,
session_id: bytes[session_id_length],
cipher_suites_length: u16,
cipher_suites: [u16; cipher_suites_length / 2],
compression_methods_length: u8,
compression_methods: bytes[compression_methods_length],
extensions_length: u16,
extensions: [Extension; fill] within extensions_length,
}
packet ServerHello {
legacy_version: u16,
random: bytes[32],
session_id_length: u8,
session_id_echo: bytes[session_id_length],
cipher_suite: u16,
compression_method: u8,
extensions_length: u16,
extensions: [Extension; fill] within extensions_length,
}
packet CertificateEntry {
cert_data_length: u24,
cert_data: bytes[cert_data_length],
extensions_length: u16,
extensions: bytes[extensions_length],
}
packet Certificate {
request_context_length: u8,
request_context: bytes[request_context_length],
certificate_list_length: u24,
certificate_list: [CertificateEntry; fill] within certificate_list_length,
}
packet CertificateVerify {
algorithm: u16,
signature_length: u16,
signature: bytes[signature_length],
}
packet Finished {
verify_data: bytes[remaining],
}
packet NewSessionTicket {
ticket_lifetime: u32,
ticket_age_add: u32,
nonce_length: u8,
nonce: bytes[nonce_length],
ticket_length: u16,
ticket: bytes[ticket_length],
extensions_length: u16,
extensions: [Extension; fill] within extensions_length,
}
# -- ハンドシェイクメッセージフレーミング --
capsule HandshakeMessage {
msg_type: HandshakeType,
length: u24,
payload: match msg_type within length {
1 => ClientHello {
legacy_version: u16,
random: bytes[32],
session_id_length: u8,
session_id: bytes[session_id_length],
cipher_suites_length: u16,
cipher_suites: [u16; cipher_suites_length / 2],
compression_methods_length: u8,
compression_methods: bytes[compression_methods_length],
extensions_length: u16,
extensions: [Extension; fill] within extensions_length,
},
2 => ServerHello {
legacy_version: u16,
random: bytes[32],
session_id_length: u8,
session_id_echo: bytes[session_id_length],
cipher_suite: u16,
compression_method: u8,
extensions_length: u16,
extensions: [Extension; fill] within extensions_length,
},
4 => NewSessionTicket {
ticket_lifetime: u32,
ticket_age_add: u32,
nonce_length: u8,
nonce: bytes[nonce_length],
ticket_length: u16,
ticket: bytes[ticket_length],
extensions_length: u16,
extensions: [Extension; fill] within extensions_length,
},
11 => Certificate {
request_context_length: u8,
request_context: bytes[request_context_length],
certificate_list_length: u24,
certificate_list: [CertificateEntry; fill] within certificate_list_length,
},
15 => CertificateVerify {
algorithm: u16,
signature_length: u16,
signature: bytes[signature_length],
},
20 => Finished { verify_data: bytes[remaining] },
_ => Unknown { data: bytes[remaining] },
},
}
# -- TLS レコード層 --
capsule TlsRecord {
content_type: ContentType,
legacy_version: u16,
length: u16,
payload: match content_type within length {
22 => Handshake { data: bytes[remaining] },
21 => Alert { level: u8, description: u8 },
20 => ChangeCipherSpec { value: u8 },
23 => ApplicationData { data: bytes[remaining] },
_ => Unknown { data: bytes[remaining] },
},
}使われている機能
| 機能 | 該当箇所 |
|---|---|
マッチタグとしての enum | match msg_type within length -- msg_type: HandshakeType |
カプセルタグとしての enum | match content_type within length -- content_type: ContentType |
u24 プリミティブ | HandshakeMessage の length: u24、cert_data_length: u24 |
[T; fill] within EXPR | extensions: [Extension; fill] within extensions_length |
| ネストしたカプセル | TlsRecord -> HandshakeMessage -> Extension |
| 固定長バイト列 | random: bytes[32] |
| 長さプレフィックス付きバイト列 | cert_data: bytes[cert_data_length] |
| 計算された配列カウント | cipher_suites: [u16; cipher_suites_length / 2] |
bytes[remaining] | Finished の verify_data: bytes[remaining] |
| 整数リテラルによるマッチ | ContentType タグに対して 22 => Handshake { ... } |
各機能の解説
マッチタグとしての enum
capsule TlsRecord {
content_type: ContentType,
...
payload: match content_type within length {
22 => Handshake { ... },
21 => Alert { ... },
...
},
}enum 型のフィールドをカプセルやフレームのマッチタグに使うと、wirespec は基底の整数型(ここでは u8)を読み取り、整数値でディスパッチします。ブランチパターンは整数リテラルで記述します。wirespec は列挙型の基底型を認識しており、適切に比較を行います。
将来的には列挙型バリアント名でのパターンマッチにも対応予定ですが、現時点では enum ブロックで定義した数値に直接マッチさせる形式です。
同じパターンは HandshakeMessage にも使われています。msg_type: HandshakeType(同じく u8)がハンドシェイク型のディスパッチを駆動します。
u24 プリミティブ
capsule HandshakeMessage {
msg_type: HandshakeType,
length: u24,
...
}TLS はハンドシェイクメッセージの長さと証明書リストの長さに 3 バイト(24 ビット)の符号なし整数を使います。wirespec の u24 は正確に 3 バイトを読み、uint32_t に格納します。モジュールのエンディアン(ここではビッグエンディアン)に従い、以下のようにアセンブルします。
value = (byte[0] << 16) | (byte[1] << 8) | byte[2]シリアライズ時も同じ順序で 3 バイトを書き出します。24 ビットに収まらない値(>= 2^24)は WIRESPEC_ERR_OVERFLOW になります。
u24be と u24le も用意されており、@endian に頼らず明示的にエンディアンを指定できます。
[T; fill] within EXPR
extensions: [Extension; fill] within extensions_length,extensions_length バイトに収まるだけの Extension をパースする構造です。コンパイラは extensions_length バイトのサブカーソルを作成し、サブカーソルが尽きるまで Extension パーサをループで呼び出します。結果は C 構造体内の固定容量配列に格納されます。
デフォルトの配列容量は WIRESPEC_MAX_ARRAY_ELEMENTS(64)です。@max_len(N) でフィールドごとにオーバーライドできます。
@max_len(32)
extensions: [Extension; fill] within extensions_length,Extension の途中でサブカーソルが尽きた場合は WIRESPEC_ERR_SHORT_BUFFER を返します。最後のアイテム完了後にサブカーソルに未消費バイトが残っている場合、それらは暗黙に破棄されます(ループが境界内に収まっていれば within の契約は満たされます)。
TLS 1.3 定義では 3 箇所に登場します。
ClientHello.extensions--extensions_length: u16で区切りServerHello.extensions-- 同上Certificate.certificate_list--certificate_list_length: u24で区切り
ネストしたカプセル
TlsRecord
└─ payload: Handshake { data: bytes[remaining] }
| (アプリケーション側で data を HandshakeMessage としてデコード)
HandshakeMessage
└─ payload: ClientHello { ... extensions: [Extension; fill] within ... }
|
Extension
└─ payload: SupportedVersions / KeyShare / ...TLS レコード層では、ハンドシェイクコンテンツは bytes[remaining] として受け渡されます。レコードパーサはハンドシェイクの中身には踏み込みません。アプリケーション側がバイトスパンを tls_tls13_handshake_message_parse に渡します。これは意図的な設計です。TLS レコードの再アセンブリ(複数レコードにまたがるハンドシェイクメッセージ、1 レコードに複数メッセージ)はワイヤパーサより上位のレイヤーで行うものです。
拡張は各ハンドシェイクパケット内で [Extension; fill] within extensions_length としてインラインでパースされます。
計算された配列カウント
cipher_suites_length: u16,
cipher_suites: [u16; cipher_suites_length / 2],暗号スイートリストは要素数ではなくバイト長としてエンコードされています。各 u16 が 2 バイトなので、2 で割れば要素数になります。式 cipher_suites_length / 2 はパース/シリアライズ時に評価されます。cipher_suites_length が奇数の場合、カウントは切り捨てられ 1 バイトが未消費のまま残る可能性がありますが、実際の TLS 実装は偶数の長さを保証しており、必要なら require 節で強制できます。
コンパイル
wirespec compile examples/tls/tls13.wspec -o build/build/tls_tls13.h と build/tls_tls13.c が生成されます。
生成される C API
/* レコード層 */
wirespec_result_t tls_tls13_tls_record_parse(
const uint8_t *buf, size_t len,
tls_tls13_tls_record_t *out, size_t *consumed);
wirespec_result_t tls_tls13_tls_record_serialize(
const tls_tls13_tls_record_t *in,
uint8_t *buf, size_t cap, size_t *written);
/* ハンドシェイクフレーミング */
wirespec_result_t tls_tls13_handshake_message_parse(
const uint8_t *buf, size_t len,
tls_tls13_handshake_message_t *out, size_t *consumed);
/* Extension TLV */
wirespec_result_t tls_tls13_extension_parse(
const uint8_t *buf, size_t len,
tls_tls13_extension_t *out, size_t *consumed);使用例
#include "tls_tls13.h"
void process_tls_record(const uint8_t *buf, size_t len) {
tls_tls13_tls_record_t rec;
size_t consumed;
wirespec_result_t r = tls_tls13_tls_record_parse(buf, len, &rec, &consumed);
if (r != WIRESPEC_OK) return;
if (rec.kind != TLS_TLS13_TLS_RECORD_HANDSHAKE) return;
/* ハンドシェイクデータは buf へのゼロコピービュー */
const wirespec_bytes_t *hs_data = &rec.handshake.data;
tls_tls13_handshake_message_t msg;
size_t msg_consumed;
r = tls_tls13_handshake_message_parse(
hs_data->ptr, hs_data->len, &msg, &msg_consumed);
if (r != WIRESPEC_OK) return;
if (msg.kind == TLS_TLS13_HANDSHAKE_MESSAGE_CLIENT_HELLO) {
/* パース済み拡張をイテレート */
for (size_t i = 0; i < msg.client_hello.extensions_count; i++) {
tls_tls13_extension_t *ext = &msg.client_hello.extensions[i];
if (ext->kind == TLS_TLS13_EXTENSION_SUPPORTED_VERSIONS) {
handle_supported_versions(ext->supported_versions.data.ptr,
ext->supported_versions.data.len);
}
}
}
}