Skip to content

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 定義

wire
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] },
    },
}

使われている機能

機能該当箇所
マッチタグとしての enummatch msg_type within length -- msg_type: HandshakeType
カプセルタグとしての enummatch content_type within length -- content_type: ContentType
u24 プリミティブHandshakeMessagelength: u24cert_data_length: u24
[T; fill] within EXPRextensions: [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]Finishedverify_data: bytes[remaining]
整数リテラルによるマッチContentType タグに対して 22 => Handshake { ... }

各機能の解説

マッチタグとしての enum

wire
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 プリミティブ

wire
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 になります。

u24beu24le も用意されており、@endian に頼らず明示的にエンディアンを指定できます。

[T; fill] within EXPR

wire
extensions: [Extension; fill] within extensions_length,

extensions_length バイトに収まるだけの Extension をパースする構造です。コンパイラは extensions_length バイトのサブカーソルを作成し、サブカーソルが尽きるまで Extension パーサをループで呼び出します。結果は C 構造体内の固定容量配列に格納されます。

デフォルトの配列容量は WIRESPEC_MAX_ARRAY_ELEMENTS(64)です。@max_len(N) でフィールドごとにオーバーライドできます。

wire
@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 としてインラインでパースされます。

計算された配列カウント

wire
cipher_suites_length: u16,
cipher_suites:        [u16; cipher_suites_length / 2],

暗号スイートリストは要素数ではなくバイト長としてエンコードされています。各 u16 が 2 バイトなので、2 で割れば要素数になります。式 cipher_suites_length / 2 はパース/シリアライズ時に評価されます。cipher_suites_length が奇数の場合、カウントは切り捨てられ 1 バイトが未消費のまま残る可能性がありますが、実際の TLS 実装は偶数の長さを保証しており、必要なら require 節で強制できます。

コンパイル

bash
wirespec compile examples/tls/tls13.wspec -o build/

build/tls_tls13.hbuild/tls_tls13.c が生成されます。

生成される C API

c
/* レコード層 */
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);

使用例

c
#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);
            }
        }
    }
}

次のステップ

  • BLE ATT -- リトルエンディアン、型エイリアス、bytes[remaining]
  • MQTT -- 継続ビット VarInt、式ベースのカプセルタグ、条件フィールド
  • チェックサム -- @checksum(internet)、CRC32、CRC32C、Fletcher-16