Closed29

TLS1.3に関するまとめ

KIDANI AkitoKIDANI Akito

A.1 Recordプロトコル

  • TLSは、アプリレイヤとTCPなどの通信レイヤの中間に位置する
  • TLSの役割:アプリケーションデータの完全性と暗号化
  • TLSは2つの層に別れている:Recordプロトコルと各サブプロトコル(Handshake, Application Data, Alert)
  • TLS=暗号化プロトコルの実装のためのフレームワーク、必要に応じて拡張可能
KIDANI AkitoKIDANI Akito

A.1.1 Record Structure

RFC 8446 Appendix B.1
enum {
    invalid(0),
    change_cipher_spec(20),
    alert(21),
    handshake(22),
    application_data(23),
    heartbeat(24),  /* RFC 6520:2014年Heartbleed脆弱性の原因 */
    (255)
} ContentType;

// ハンドシェイク開始時点
struct {
    ContentType type;
    ProtocolVersion legacy_record_version;
    uint16 length;
    opaque fragment[TLSPlaintext.length];
} TLSPlaintext;

// パラメータのネゴシエーション完了後
struct {
    ContentType opaque_type = application_data; /* 23 */
    ProtocolVersion legacy_record_version = 0x0303; /* TLS v1.2 */
    uint16 length;
    opaque encrypted_record[TLSCiphertext.length];
} TLSCiphertext;

// 暗号化されるペイロード:いずれかのサブプロトコルのデータ
struct {
    opaque content[TLSPlaintext.length];
    ContentType type;
    uint8 zeros[length_of_padding];
} TLSInnerPlaintext;
  • TLSレコードのバージョン番号は利用されなくなった
    • TLS1.2まではClientHelloとTLSレコードでプロトコルバージョンを表示
    • 過去に互換性の問題多数:ファイアウォールなどが未知のバージョンのトラフィック破棄
    • TLS1.3では0x0303(TLS1.2)で固定
      • 互換性を考慮し、ClientHelloでのみ0x0301(TLS1.0)も利用可
  • TLSCiphertextではtype -> opaque_type(不透明な型)に変更
    • TLS1.2以前でサブプロトコルを示す値が暗号化されず問題:攻撃やフィルタリング
    • 実際に利用されないのでapplication_data(23)で固定
    • サブプロトコルを示す値は以下のように暗号化される
  • TLSInnerPlaintextは長さを示すフィールドがない
    • 後ろからNUL(ゼロのみのバイト列)を除いて、1バイトのConetntTypeを見てサブプロトコルを判断
KIDANI AkitoKIDANI Akito

A.1.2 暗号化

  • Recordプロトコルの目的:ペイロードの機密性と完全性の確保
    • TLS1.3では認証付き暗号(AEAD, authenticated encryption [with associated data])を利用
    • TLS1.2以前では暗号とは別の部分で完全性検証(例:CBCモード)
      • プロトコルが複雑化、(CBCは)脆弱性の温床(BEAST、Lucky 13、POODLE、Sweet32など)
    • AEADのパラメータ3つ:ナンス、平文、付加的なデータ
RFC 8446 5.2. Record Payload Protection
AEADEncrypted = AEAD-Encrypt(write_key, nonce, plaintext, additional_data)

additional_data = TLSCiphertext.opaque_type ||
                  TLSCiphertext.legacy_record_version ||
                  TLSCiphertext.length

plaintext of encrypted_record =
    AEAD-Decrypt(peer_write_key, nonce,
                 additional_data, AEADEncrypted)
  • ナンス:同じ平文が同じ値にならないために利用。

    • TLSでは64ビットのメッセージカウンタを利用し、リプレイ攻撃にも対処
  • 付加的なデータ:TLSではメッセージのうち暗号化されないもの

    • opaque_type + legacy_record_version + length
    • 暗号化に利用することで改竄を防ぐ
  • 鍵の利用制限

    • 同じ鍵で暗号化できるデータ量には上限がある
    • 同じ接続が維持された場合に備えて、TLS1.3ではパラメータをリセットし鍵を更新する機構がある
KIDANI AkitoKIDANI Akito

A.1.3 長さを隠す

  • TLS1.2:暗号化された状態を観察するとメッセージの長さから平文の長さを予測できる
    • 例:パスワードの長さなども予測できる
    • 常時データ送信するだけでメッセージの長さを隠せる
  • TLS1.3:平文の長さは隠されている(TLSCiphertext参照)
  • Recordプロトコルのメッセージ自体をパディングして全メッセージの長さを揃えることも可能[1]

A.1.4 メッセージのフラグメンテーション

  • Recordプロトコルのペイロード上限:16384バイト
    • サブプロトコルのデータの格納方法は規定なし
    • 複数格納可 <-> 1つを分割して複数のレコードにすることも可能
      • ハンドシェイクメッセージは他のサブプロトコルと混ぜられない
      • Alertプロトコルはフラグメンテーションすべきではない

A.1.5 さまざまなサブプロトコル

  • TLS1.3には4つのサブプロトコルがある
  • Alertプロトコル:例外的な状況で使われる
  • Application Dataプロトコル:データ転送に利用
  • Change Cipher Specプロトコル:脆弱性があったため非推奨。互換性のため維持されており、送信は可能だが受信側で無視される。
    • Handshakeプロトコルではないため、完全性の検証が行われずOpenSSLの脆弱性があった(プロフェッショナルSSL/TLS p.152)
  • Handshakeプロトコル:セキュリティパラメータをネゴシエートする
脚注
  1. Bulletproof TLS and PKI 2nd Edition, p. 343によると、この機能はあまり使われていないとのこと。 ↩︎

KIDANI AkitoKIDANI Akito

A.2 Handshakeプロトコル

  • TLSは未暗号化状態で開始
    • ハンドシェイクでセキュリティパラメータを決定
  • 達成すべきポイント4つ
    1. セキュリティパラメータを双方が合意
    2. サーバ(と場合によってクライアント)の真正性検証
    3. 暗号鍵の生成(機密性)
    4. 能動的ネットワーク攻撃者による書き換えがないことの検証(完全性)
Handshakeプロトコルの構造(シンプルバージョン)
struct {
      HandshakeType msg_type;
      uint24 length;
      HandshakeMessage message;
} Handshake;
RFC 8446 TLS1.3, 4. Handshake Protocol
enum {
    client_hello(1), // TLS1.2と同じ
    server_hello(2), // TLS1.2と同じ
    new_session_ticket(4), // RFC 5077: Stateless TLS Session Resumption 
    end_of_early_data(5), // TLS1.3で追加
    encrypted_extensions(8), // TLS1.3で追加
    certificate(11), // TLS1.2と同じ
    // server_key_exchange (12)はTLS1.3で廃止
    certificate_request(13), // TLS1.2と同じ
    // server_hello_done(14)はTLS1.3で廃止
    certificate_verify(15), // TLS1.2と同じ
    // client_key_exchange(16)はTLS1.3で廃止
    finished(20), // TLS1.2と同じ
    key_update(24), // TLS1.3で追加
    message_hash(254),// TLS1.3で追加
    (255) // TLS1.2と同じ
} HandshakeType;

struct {
    HandshakeType msg_type;    /* handshake type */
    uint24 length;             /* remaining bytes in message */
    select (Handshake.msg_type) {
        case client_hello:          ClientHello;
        case server_hello:          ServerHello;
        case end_of_early_data:     EndOfEarlyData;
        case encrypted_extensions:  EncryptedExtensions;
        case certificate_request:   CertificateRequest;
        case certificate:           Certificate;
        case certificate_verify:    CertificateVerify;
        case finished:              Finished;
        case new_session_ticket:    NewSessionTicket;
        case key_update:            KeyUpdate;
    };
} Handshake;
  • ある時点で送受信可能なメッセージ種類には制限がある
    • 違反すると接続は終了する
KIDANI AkitoKIDANI Akito
  • 主要なハンドシェイクと拡張を要約すると以下
基本のフルハンドシェイク
       Client                                           Server

鍵  ^ ClientHello
交 | + key_share*| + signature_algorithms*
     | + psk_key_exchange_modes*
     v + pre_shared_key*       -------->
                                                  ServerHello  ^+ key_share*  |+ pre_shared_key*  v 換
                                        {EncryptedExtensions}  ^  サーバ
                                        {CertificateRequest*}  v  パラメータ
                                               {Certificate*}  ^
                                         {CertificateVerify*}  | 認証
                                                   {Finished}  v
                               <--------  [Application Data*]
     ^ {Certificate*}
認証 | {CertificateVerify*}
     v {Finished}              -------->
       [Application Data]      <------->  [Application Data]

*記号:必ず送信されるわけではない
{}記号:[sender]_handshake_traffic_secretから導出された鍵で保護される
[]記号:[sender]_application_traffic_secret_Nから導出された鍵で保護される
鍵交換:鍵を導出する材料を共有
サーバパラメータ:鍵以外
認証:サーバ・クライアントの認証、鍵の確認、完全性検証

KIDANI AkitoKIDANI Akito
事前共有鍵を利用するハンドシェイク
          Client                                               Server

   初回:
          ClientHello
          + key_share               -------->
                                                          ServerHello
                                                          + key_share
                                                {EncryptedExtensions}
                                                {CertificateRequest*}
                                                       {Certificate*}
                                                 {CertificateVerify*}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Certificate*}
          {CertificateVerify*}
          {Finished}                -------->
                                    <--------      [NewSessionTicket]
          [Application Data]        <------->      [Application Data]


   2回目以降:
          ClientHello
          + key_share*
          + pre_shared_key          -------->
                                                          ServerHello
                                                     + pre_shared_key
                                                         + key_share*
                                                {EncryptedExtensions}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Finished}                -------->
          [Application Data]        <------->      [Application Data]
KIDANI AkitoKIDANI Akito
0-RTTのハンドシェイク
         Client                                               Server

         ClientHello
         + early_data
         + key_share*
         + psk_key_exchange_modes
         + pre_shared_key
         (Application Data*)     -------->
                                                         ServerHello
                                                    + pre_shared_key
                                                        + key_share*
                                               {EncryptedExtensions}
                                                       + early_data*
                                                          {Finished}
                                 <--------       [Application Data*]
         (EndOfEarlyData)
         {Finished}              -------->
         [Application Data]      <------->        [Application Data]

()記号:client_early_traffic_secretから導出された鍵で保護されるメッセージ

  • EndOfEarlyDataは初期設計ではアラートプロトコルだったがdraft 19でハンドシェイクに変更された(参照
KIDANI AkitoKIDANI Akito

A.2.1 鍵交換

  • TLS1.2のハンドシェイクと似ているが完全に別物:以前より複雑
    • 互換性を維持しつつ、将来の強力さを担保
  • クライアントからの通信開始によるパフォーマンス改善
    • 即座に鍵交換開始可能:当然サーバの要求する機能は未把握
      • <-> TLS1.2:ServerHelloDoneを待ってClientKeyExchange
    • クライアントは1つ以上の鍵交換手法を提案することで解決
      • サーバが応じられない場合はHelloRetryRequestで応答(詳細は後述)
  • 0-RTTによるパフォーマンス改善
    • 以前やりとりしたクライアントはClientHello直後に暗号化データ送信可能(上の図を参照)
    • セキュリティに影響あり(詳細は後述)
  • サーバ拡張の暗号化
    • クライアントの提案に合意した場合ServerHelloで応答
      • セキュリティパラメータの選択結果を連絡+鍵交換の素材
    • 攻撃者に有利となるサーバ拡張はEncryptedExtensionsメッセージで暗号化して送信
  • ハンドシェイク以降で使うメッセージ
    • なるべく暗号化するため、ハンドシェイク後に送信されるよう改良
      • クライアント認証のCertificateメッセージ[1]やセッションリザンプションのためのNewSessionTicketメッセージ[2]
脚注
  1. TLS1.2ではClientKeyExchangeの前に送られることとなっていた。RFC 5246https://www.rfc-editor.org/rfc/rfc5246 ↩︎

  2. TLS1.1時代に策定されたRFC 5077ではChangeCipherSpecの直前に送られることとなっていた。 ↩︎

KIDANI AkitoKIDANI Akito
■暗号に関するネゴシエーション
  • どのメッセージが何のためにやりとりされるのかが重要
    • 暗号化アルゴリズム・ハッシュ関数の選択、鍵交換、ハンドシェイクメッセージの認証
  • 暗号化アルゴリズム・ハッシュ関数
    • ネゴシエーションされた暗号スイートで決定
    • ClientHelloで一覧指定、サーバが1つ選択
  • 鍵交換
key_share拡張のextension_dataはKeyShareClientHello
      struct {
          KeyShareEntry client_shares<0..2^16-1>;
      } KeyShareClientHello;

      struct {
          NamedGroup group;
          opaque key_exchange<1..2^16-1>;
      } KeyShareEntry;
  • NamedGroupは名前付きグループ
    • FFDH RFC 7919ではffdhe2048、ffdhe3072など
    • ECDH)ではsecp256r1、X25519など
    • サーバがClientHelloのグループに合意できない場合、supported_groups拡張で対応可能グループを伝達
  • 署名アルゴリズム
    • サーバがsignature_algorithms拡張で伝達
    • signature_algorithms_cert拡張を利用すると証明書署名に別アルゴリズムを指定可能
  • 事前共有鍵
    • TLS1.3の認証:証明書もしくはセッションリザンプション用の事前共有鍵(pre-shared keys)
    • psk_key_exchange_modes拡張:事前共有鍵利用時にクライアントは指定必須。サーバは本拡張送信不可。
RFC 8446 4.2.9. Pre-Shared Key Exchange Modes
      enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode;

      struct {
          PskKeyExchangeMode ke_modes<1..255>;
      } PskKeyExchangeModes;

   psk_ke:  PSK-only key establishment.  In this mode, the server
      MUST NOT supply a "key_share" value.

   psk_dhe_ke:  PSK with (EC)DHE key establishment. 
  • pre_shared_key拡張:事前共有鍵のIDを指定
RFC 8446 4.2.11. Pre-Shared Key Extension
      struct {
          opaque identity<1..2^16-1>;
          uint32 obfuscated_ticket_age;
      } PskIdentity;

      opaque PskBinderEntry<32..255>;

      struct {
          PskIdentity identities<7..2^16-1>;
          PskBinderEntry binders<33..2^16-1>;
      } OfferedPsks;

      struct {
          select (Handshake.msg_type) {
              case client_hello: OfferedPsks;
              case server_hello: uint16 selected_identity;
          };
      } PreSharedKeyExtension;
脚注
  1. RFC 8446ではクライアントが空のkey_share拡張を送ることでサーバに鍵交換アルゴリズムを選択させる場合もあると書かれている。もちろん追加のラウンドトリップが必要。 ↩︎

KIDANI AkitoKIDANI Akito
■ClientHelloメッセージ
  • 旧バージョンとだいたい同じだが、いくつかのフィールドは廃止
    • 利用されるのは3つ:random、cipher_suites、extensions
    • フォーマットは同じ:旧バージョンへのフォールバックによる相互運用+ミドルボックス対策
RFC 8446 4.1.2. Client Hello
      uint16 ProtocolVersion;
      opaque Random[32];

      uint8 CipherSuite[2];    /* Cryptographic suite selector */

      struct {
          ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
          Random random;
          opaque legacy_session_id<0..32>;
          CipherSuite cipher_suites<2..2^16-2>;
          opaque legacy_compression_methods<1..2^8-1>;
          Extension extensions<8..2^16-1>;
      } ClientHello;
  • プロトコルバージョンの扱い
    • 初期ドラフト:Recordプロトコル、ClientHelloともに相互接続性の問題あり
    • 拡張を利用してプロトコルバージョンをネゴシエーションする新しい仕組みで解決(supported_versions拡張)
  • Randomフィールド
    • 32バイトの暗号学的にランダムなデータ
    • 旧バージョンから継続して利用される
  • 旧バージョンのSession IDフィールド
    • 新しいセッションリザンプションの仕組みができたので不要となった
  • 暗号スイート
    • 旧バージョンから継続して利用される
  • 旧バージョンのCompression methodフィールド
    • 圧縮は情報漏洩につながるため、圧縮なしの0のみ指定可能
KIDANI AkitoKIDANI Akito
■ServerHelloメッセージ
  • ハンドシェイク完遂のための必要情報の伝達
    • 選択したプロトコルバージョン、暗号スイートを含む
RFC 8446 4.1.3. Server Hello
      struct {
          ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
          Random random;
          opaque legacy_session_id_echo<0..32>;
          CipherSuite cipher_suite;
          uint8 legacy_compression_method = 0;
          Extension extensions<6..2^16-1>;
      } ServerHello;
  • Randomの役割の変化
    • 本来:ハンドシェイクを一意にする乱数
    • 追加:
      • HelloRetryRequestを示す特別な乱数(後述)
      • ダウングレード攻撃保護:以下のrandom値の場合接続中止
        • TLS1.3対応サーバがTLS1.2でネゴシエートする場合、末尾が44 4F 57 4E 47 52 44 01(DOWNGRD 01を示す)
        • 同じくTLS1.1以前の場合は 44 4F 57 4E 47 52 44 00
          • この仕組みはTLS1.2対応サーバでも利用推奨(SHOULD)とされている(TLS1.3仕様でのTLS1.2仕様の更新)
  • 旧バージョンに比べて簡潔化:暗号化のため後続メッセージ新設(EncryptedExtensions)
  • legacy〜は構造体を揃えるためのフィールド
    • legacy_session_id_echo:クライアントから送られた値をそのまま返す(エコー)
      • セッションリザンプション用データは暗号化されてNewSessionTicketメッセージで共有
    • legacy_version:実体はsupported_versions拡張
■HelloRetryRequestメッセージ
  • TLS1.3クライアントはサーバの鍵交換アルゴリズムを推測、失敗することもある
  • 失敗時はHelloRetryRequestで解決方法を伝達
  • Randomフィールドを以下の特別な値とする
     // HelloRetryRequestのSHA-256ハッシュ値
     CF 21 AD 74 E5 9A 61 11 BE 1D 8C 02 1E 65 B8 91
     C2 A2 11 16 7A BB 8C 5E 07 9E 09 E2 C8 A8 33 9C
  • ClientHelloに含まれていないTLS拡張を含めてはならない
    • 例外:cookie拡張[1]
  • 同一接続上で2回目のHelloRetryRequestを受信したクライアントはunexpected_messageアラートでハンドシェイクを中止する
  • HelloRetryRequest受信後のServerHelloで拡張の値(例:supported_versionsなど)に変更があった場合クライアントはillegal_parameterアラートでハンドシェイクを中止する
脚注
  1. RFC 8446 TLS1.3 4.2.2. Cookie:cookieの目的は2つ。DoS対策とクライアントへの状態のオフロード(ClientHelloのハッシュ値をcookieに含める)。クライアントは新しいClientHelloを送る際に本拡張の値をcookie拡張に含め送信する。 ↩︎

KIDANI AkitoKIDANI Akito
TLS拡張
RFC 8446 TLS1.3 4.2. Extensions
    struct {
        ExtensionType extension_type;
        opaque extension_data<0..2^16-1>;
    } Extension;

    enum {
        server_name(0),                             /* RFC 6066 CH, EE */
        max_fragment_length(1),                     /* RFC 6066  CH, EE  */
        status_request(5),                          /* RFC 6066 CH, CR, CT */
        supported_groups(10),                       /* RFC 8422, 7919 CH, EE */
        signature_algorithms(13),                   /* RFC 8446 CH, CR */
        use_srtp(14),                               /* RFC 5764 CH, EE */
        heartbeat(15),                              /* RFC 6520 CH, EE */
        application_layer_protocol_negotiation(16), /* RFC 7301 CH, EE */
        signed_certificate_timestamp(18),           /* RFC 6962 CH, CR, CT */
        client_certificate_type(19),                /* RFC 7250 CH, EE */
        server_certificate_type(20),                /* RFC 7250 CH, EE */
        padding(21),                                /* RFC 7685 CH */
        pre_shared_key(41),                         /* RFC 8446 CH, SH */
        early_data(42),                             /* RFC 8446 */
        supported_versions(43),                     /* RFC 8446 CH, SH, HRR*/
        cookie(44),                                 /* RFC 8446 HRR */
        psk_key_exchange_modes(45),                 /* RFC 8446 CH */
        certificate_authorities(47),                /* RFC 8446 CR */
        oid_filters(48),                            /* RFC 8446 CR */
        post_handshake_auth(49),                    /* RFC 8446 CH */
        signature_algorithms_cert(50),              /* RFC 8446 CH, CR */
        key_share(51),                              /* RFC 8446 CH, SH, HRR  */
        (65535)
    } ExtensionType;
  • 拡張の順序は問わない
    • ただしpre_shared_key拡張はClientHelloの最後の拡張でなくてはならない(ServerHelloは除く)
    • 1メッセージに同じ拡張が含まれてはならない
supported_versions拡張の値の例
      uint16 ProtocolVersion;
      struct {
          select (Handshake.msg_type) {
              case client_hello:
                   ProtocolVersion versions<2..254>;

              case server_hello: /* and HelloRetryRequest */
                   ProtocolVersion selected_version;
          };
      } SupportedVersions;
KIDANI AkitoKIDANI Akito

A.3 認証

  • TLS1.2以前:サーバ認証と鍵交換のためのメッセージが結合(参考:プロフェッショナルSSL/TLS 2.4 認証
  • TLS1.3では認証のメッセージ群が他のメッセージから切り離されて統一された
  • 認証関係のメッセージ2つ:Certificate+CertificateVerify
KIDANI AkitoKIDANI Akito

A.3.1 トランスクリプトハッシュ

  • TLSにおける認証の核:トランスクリプトハッシュ
    • ハンドシェイクの一連のメッセージを連結したもののハッシュを計算
    • Recordプロトコルのヘッダを除く
    • HelloRetryRequestは特殊:cookie拡張に最初のClientHelloメッセージのハッシュが入る

A.3.2 Certificateメッセージ

  • 複数の証明書を格納可能
RFC 8446 TLS1.3 4.4.2. Certificate
      enum {
          X509(0),
          RawPublicKey(2),
          (255)
      } CertificateType;

      struct {
          select (certificate_type) {
              case RawPublicKey:
                /* From RFC 7250 ASN.1_subjectPublicKeyInfo */
                opaque ASN1_subjectPublicKeyInfo<1..2^24-1>;

              case X509:
                opaque cert_data<1..2^24-1>;
          };
          Extension extensions<0..2^16-1>;
      } CertificateEntry;

      struct {
          opaque certificate_request_context<0..2^8-1>;
          CertificateEntry certificate_list<0..2^24-1>;
      } Certificate;
  • Extensionで個々の証明書に任意の拡張を付与
    • OCSP(Online Certificate Status Protocol):失効情報
      • TLS1.2ではCertificateStatusハンドシェイクメッセージが利用されていた
      • 事前に取得するためのstatus_request_v2拡張(RFC6961)があったがTLS1.3では廃止(参考:拡張一覧
    • SCT(Signed Certificate Timestamps):証明書の透明性を証明するタイムスタンプ
  • TLS1.2までは証明書チェーンが順番通りでないとダメ[1]
  • TLS1.3ではサーバ証明書が先頭にあれば良い:現実に即した形に
  • クライアント証明書:サーバが送信するCertificateRequestメッセージのcertificate_reqeust_contextフィールドを使うと1接続で複数の証明書を使って認証できる(後述)
脚注
  1. RFC 5246 TLS1.2では次のように書かれている:The sender's certificate MUST come first in the list. Each following certificate MUST directly certify the one preceding it. ↩︎

KIDANI AkitoKIDANI Akito

A.3.3 CertificateVerifyメッセージ

  • 送信した証明書に対応する秘密鍵の所有を証明する
// TLS1.3
      struct {
          SignatureScheme algorithm;
          opaque signature<0..2^16-1>;
      } CertificateVerify;

// TLS1.2
      struct {
           digitally-signed struct {
               opaque handshake_messages[handshake_messages_length];
           }
      } CertificateVerify;
  • SignatureSchemeについてはB.3.1.3. Signature Algorithm Extension
    • rsa_pkcs1_sha256(0x0401)、ecdsa_secp256r1_sha256(0x0403)、ed25519(0x0807)など
  • 署名対象のデータは以下のように作成する
    • 64個のスペース(0x20):TLS1.2でのClientHello.randomを利用した選択プレフィクス衝突攻撃の問題への対処[1]
    • 署名目的:「TLS1.3, server CertificateVerify」or「TLS1.3,client CertificateVerify」
    • 1バイトのゼロ(0x00)
    • トランスクリプトハッシュ:ここまでのハンドシェイクメッセージのハッシュ

A.3.4 CertificateRequestメッセージ

  • クライアント認証を要求する場合にサーバが送信
// TLS1.3のCertificateRequest
      struct {
          opaque certificate_request_context<0..2^8-1>; // クライアントのCertificateメッセージにエコーされリプレイ攻撃を防止
          Extension extensions<2..2^16-1>;
      } CertificateRequest;

// TLS1.2のCertificateRequest
      struct {
          ClientCertificateType certificate_types<1..2^8-1>;
          SignatureAndHashAlgorithm
            supported_signature_algorithms<2^16-1>; // TLS1.3ではsignature_algorithmsまたはsignature_algorithms_cert拡張を利用
          DistinguishedName certificate_authorities<0..2^16-1>; // TLS1.3では同名の拡張を利用
      } CertificateRequest;
      enum {
          rsa_sign(1), dss_sign(2), rsa_fixed_dh(3), dss_fixed_dh(4),
          rsa_ephemeral_dh_RESERVED(5), dss_ephemeral_dh_RESERVED(6),
          fortezza_dms_RESERVED(20), (255)
      } ClientCertificateType;
  • context:証明書リクエストを一意にするために利用
    • ハンドシェイク後にクライアント認証を利用する場合に使う(それ以外は空)
  • 拡張:signature_algorithmsは必須、certificate_authoritiesは任意
脚注
  1. RFC8446にも確かにそう書いてあるがどういう問題だったのかいまいちピンとこない...orz。IETFのメーリングリストでのAdam Langley氏の発言がまさにこの問題を提起しているっぽいのは分かるだが...TLS1.2とのクロスプロトコル中間者攻撃を想定している? ↩︎

KIDANI AkitoKIDANI Akito

A.3.5 Finishedメッセージ

  • ハンドシェイクの最後に送信される
    • ハンドシェイクの完全性を検証するためのメッセージ
RFC 8446 TLS1.3 4.4.4. Finished
struct {
    opaque verify_data[Hash.length];
} Finished;

verify_data =
    HMAC(finished_key,
         Transcript-Hash(Handshake Context,
                       Certificate*, CertificateVerify*))
*は任意

finished_key =
   HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)
// キーについては鍵スケジュールの節で後述
  • ハッシュ関数は暗号スイートで決まる
    • SHA256(32バイト)またはSHA384(48バイト)[1]

A.3.6 Post-Handshake Authentication

  • ハンドシェイク後にクライアント認証を実行(サーバがFinishedを送った後!)
    • クライアントは対応有無をCHのpost_handshake_auth拡張で示す
      • extension_dataは空(長さ0)
    • サーバはCertificateRequestでクライアント証明書を要求
      • 事前共有鍵で認証した場合、サーバはCertificateRequestを送ってはいけない
    • クライアント認証する場合クライアントはCertificate,CertificateVerify,Finishedメッセージを送る
      • クライアント認証しない場合は、クライアントは空のCertificateメッセージとFinishedメッセージを送る
    • post_handshake_auth拡張を送っていないのにCertificateRequestを受け取ったクライアントはunexpected_messageアラートプロトコルを送信する
    • クライアント認証はユーザーのプロンプト操作が必要な場合もあるので、メッセージが遅れがち:送受信の順序が入れ替わるケースもあるので、certificate_request_contextの値でレスポンスを区別
脚注
  1. TLS1.2では12バイトがデフォルトだった(参考↩︎

KIDANI AkitoKIDANI Akito

A.3.7 事前共有鍵による認証

  • TLSは証明書以外にも事前共有鍵にも対応
    • セッションリザンプションでパフォーマンス向上させる重要な構成要素
  • サーバは証明書での認証後、クライアントに複数の事前共有鍵を発行可能
  • 事前共有鍵に対応するクライアントはpsk_key_exchange_modes拡張を提示
enum { 
  psk_ke(0), // PFSなし
  psk_dhe_ke(1), // PFSあり。ECDHE鍵交換にも対応
  (255) } PskKeyExchangeMode;

struct {
    PskKeyExchangeMode ke_modes<1..255>;
} PskKeyExchangeModes;
  • 認証に必要な情報はpre_shared_key拡張で転送
RFC 8446 TLS1.3 4.2.11. Pre-Shared Key Extension
      struct {
          opaque identity<1..2^16-1>; // クライアントの持つセッション情報またはサーバに保管されたセッションを特定するラベル(NewSessionTicket.ticket)
          uint32 obfuscated_ticket_age; // クライアントから見たCHの鮮度:リプライ攻撃防御
      } PskIdentity;

      opaque PskBinderEntry<32..255>;
      // 事前共有鍵の所有を示すHMACの値

      struct {
          PskIdentity identities<7..2^16-1>;// 事前共有鍵のID(複数可)
          PskBinderEntry binders<33..2^16-1>;
      } OfferedPsks;

      struct {
          select (Handshake.msg_type) {
              case client_hello: OfferedPsks;
              case server_hello: uint16 selected_identity;
          };
      } PreSharedKeyExtension;
  • 0-RTTの場合最初に提示する事前共有鍵でearly dataを暗号化する
  • サーバが選択した事前共有鍵で接続、ハンドシェイク用の鍵を生成

A.3.8 セッションリザンプション

  • 対応するサーバはハンドシェイク完了後チケットを送信(複数可)
struct {
    uint32 ticket_lifetime; // 寿命を秒数で表す、7日以内
    uint32 ticket_age_add; // obfuscated_tikcet_ageが平文で送信される際のフィンガープリンティング防止の値
    opaque ticket_nonce<0..255>;
    opaque ticket<1..2^16-1>;
    Extension extensions<0..2^16-2>;
} NewSessionTicket;

# 事前共有鍵の計算で利用
HKDF-Expand-Label(
        resumption_master_secret, // 鍵スケジュールで生成
        "resumption", ticket_nonce, Hash.length)
  • チケットの拡張: TLS1.3ではearly_data拡張のみ定義
    • early_dataが指定されている場合0-RTTで利用可能
  • early_dataのextension_dataはEarlyDataIndication
      struct {} Empty;

      struct {
          select (Handshake.msg_type) {
              // 0-RTTで転送可能なデータ量を指定
              case new_session_ticket:   uint32 max_early_data_size;
              case client_hello:         Empty;
              case encrypted_extensions: Empty;
          };
      } EarlyDataIndication;
KIDANI AkitoKIDANI Akito

A.4 Alertプロトコル

  • TLS1.2以前と同様、エラーの伝達、接続終了を示す
  • 接続の状態により暗号化される場合もある
  • Alert=AlertLevel(1バイト) + AlertDescription(1バイト)
    • TLS1.2と揃えるためにAlertLevelが残されているだけ
    • TLS1.3では深刻度はメッセージで暗に表され、ほぼ全てFatal(深刻)=接続中止必須
  • フラグメンテーション不可
    • TLS1.2以前ではフラグメンテーション可能で潜在的脆弱性あり
    • RFC 8444 5.1 Record Layer: Alertプロトコルのメッセージは1レコードに1つだけ
      enum { warning(1), fatal(2), (255) } AlertLevel;

      enum {
          close_notify(0),
          unexpected_message(10),
          bad_record_mac(20),
          record_overflow(22),
          handshake_failure(40),
          bad_certificate(42),
          unsupported_certificate(43),
          certificate_revoked(44),
          certificate_expired(45),
          certificate_unknown(46),
          illegal_parameter(47),
          unknown_ca(48),
          access_denied(49),
          decode_error(50),
          decrypt_error(51),
          protocol_version(70),
          insufficient_security(71),
          internal_error(80),
          inappropriate_fallback(86),
          user_canceled(90),
          missing_extension(109),
          unsupported_extension(110),
          unrecognized_name(112),
          bad_certificate_status_response(113),
          unknown_psk_identity(115),
          certificate_required(116),
          no_application_protocol(120),
          (255)
      } AlertDescription;

      struct {
          AlertLevel level;
          AlertDescription description;
      } Alert;
KIDANI AkitoKIDANI Akito

A.4.1 接続の終了

  • 強制切断攻撃truncation attack:能動的ネットワーク攻撃者によるメッセージ送受信停止
  • これを検知するための2つのAlert
    • user_canceled:プロトコル失敗以外の理由でのキャンセル連絡。ハンドシェイク完了前。
      • AlertLevelはwarning
      • 続けてclose_no
    • close_notify:暗号化のネゴシエーションが実施済みの場合の接続終了
      • と本書には書かれているが、RFC 8446読むとそうは理解できない...謎
      • これ以降に受信されたデータは無視される
KIDANI AkitoKIDANI Akito

A.5 暗号に関する計算

  • 暗号鍵:ハンドシェイクの段階で生成される
    • 原則:1つの鍵は1つの用途にしか使わない → TLS1.3では複数の鍵を規定
鍵用途 生成数 説明
binder_key 1 事前共有鍵の保持証明用
client_early_traffic_secret 1 early dataの暗号化に利用
early_exporter_main_secret 1 鍵素材をTLSの外部で利用するためのエクスポータ用(0-RTT)
server_handshake_traffic_secret, client_handshake_traffic_secret 2 ハンドシェイクのトラフィックの暗号化に利用
server_application_traffic_secretN, client_application_traffic_secret 2以上 アプリケーションのトラフィックの暗号化に利用
exporter_main_secret 1 鍵素材をTLSの外部で利用するためのエクスポータ用
resumption_main_secret 1 セッションリザンプションで使う事前共有鍵を生成する鍵
KIDANI AkitoKIDANI Akito

A.5.1 鍵の導出

  • 鍵導出key derivation:暗号に使う鍵を生成する処理
    • 鍵導出関数key derivation function, KDFとして実装:入力を鍵として利用できるようにする
  • TLS1.3ではHKDF (HMAC-based KDF, RFC-5869, 2010年)を採用
    • TLS1.2以前では独自のKDFが採用されていた
    • TLS1.3では標準化された要素技術としてHKDFを採用
  • HKDFの2段階:抽出extraction+伸張expansion
    • 抽出extraction:入力から利用可能なエントロピーを取り出す。ソルト指定可能
      • HKDF-Extract(salt, input) -> extracted_key
    • 伸張expansion:extractionの出力から、任意の長さ、個数の鍵を導出する
      • HKDF-Expand(extracted_key, context, length) -> expanded_key
      • 複数の鍵はcontextで区別
KIDANI AkitoKIDANI Akito

A.5.2 鍵スケジュール

  • 安全な通信の要件:敵対者に知られていない十分に強力な秘密情報1つの確立
    • 一般にはDiffieHellman鍵交換を利用
    • かつ、相手が正しいかを認証:主にサーバ証明書、セッションリザンプションやパスワード認証も可
  • TLS1.3ではHKDFの伸張expansionのラッパー関数を定義
    • HKDF-Expand-LabelとDerive-Secret
    • これを使った鍵導出処理を鍵スケジュールとよぶ(多分
RFC 8446 TLS1.3 7.1 Key Schedule
      // 同一の入力から別用途の鍵を複数生成できる
       HKDF-Expand-Label(Secret, Label, Context, Length) =
            HKDF-Expand(Secret, HkdfLabel, Length)

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

       Derive-Secret(Secret, Label, Messages) =
            HKDF-Expand-Label(Secret, Label,
                              Transcript-Hash(Messages), Hash.length)
  • HKDFとTranscript-Hash:暗号スイートで指定されたハッシュ関数を利用
  • Derive-Secret:鍵導出の処理をここのハンドシェイクに結びつける(トランスクリプトハッシュを利用)
    • ハンドシェイクをユニークにするクライアント/サーバの乱数のおかげで、暗号鍵もユニークになる
    • トランスクリプトハッシュはハンドシェイク
  • 鍵導出処理:各ブロックが2つの入力から成る(秘密情報とソルト)
    • 先頭のブロックではソルトは空文字列
    • 以後のブロックでは前のブロックの出力をソルトとする
  • 先頭ブロック:秘密情報は事前共有鍵もしくは空文字列を利用
RFC 8446 TLS1.3 鍵スケジュールの先頭ブロック
             0
             |
             v
   PSK ->  HKDF-Extract = Early Secret 
             | // まずEarly Secretを生成し、3つの鍵をexpansionする
             +-----> 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 // 状態の再利用を防ぐためにexpansionし、出力を次のブロックのソルトとする
       Derive-Secret(., "derived", "")
             |
             v
  • 2つめのブロック:秘密情報はECDHE/DHE鍵交換の出力値、または空文字列
RFC 8446 TLS1.3 鍵スケジュールの2つめのブロック
       Derive-Secret(., "derived", "") // 先頭ブロックの最後の処理
             |
             v
   (EC)DHE -> HKDF-Extract = Handshake Secret
             | // Handshake Secretを生成し、2つの鍵をexpansion
             +-----> Derive-Secret(., "c hs traffic",
             |                     ClientHello...ServerHello)
             |                     = client_handshake_traffic_secret
             |
             +-----> Derive-Secret(., "s hs traffic",
             |                     ClientHello...ServerHello)
             |                     = server_handshake_traffic_secret
             v // 先頭ブロック同様に再利用を防ぐexpansion
       Derive-Secret(., "derived", "")
             |
             v
  • 3つめのブロック:秘密情報は空文字列
RFC 8446 TLS1.3 鍵スケジュールの3つめのブロック
      Derive-Secret(., "derived", "") // 2つめのブロックの最後の処理
             |
             v
   0 -> HKDF-Extract = Master Secret
             | // Master Secretを生成し、expansionする
             +-----> 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
  • client_application_traffic_secret_0の0は1つめの暗号鍵を意味する
    • 最初のハンドシェイク完了後にKeyUpdateメッセージを送ることで、暗号鍵を更新できる
RFC 8446 TLS1.3 7.2. Updating Traffic Secrets
       application_traffic_secret_N+1 =
           HKDF-Expand-Label(application_traffic_secret_N,
                             "traffic upd", "", Hash.length)
RFC8446 TLS1.3 4.6.3. KeyUpdateハンドシェイクメッセージ
      enum {
          update_not_requested(0), update_requested(1), (255)
      } KeyUpdateRequest;

      struct {
          KeyUpdateRequest request_update;
      } KeyUpdate;
  • KeyUpdateハンドシェイクメッセージ:双方がFinishedメッセージ送信後に送信可能

    • Finished受信前にKeyUpdateを受信した場合はunexpected_messageアラートで接続終了
  • KeyUpdate送信後は次の世代の鍵で暗号化してメッセージを送信

    • KeyUpdateを受信した側は、update_not_requestedでKeyUpdateを送信してから次のアプリケーションデータを送信する
    • 双方が同時にKeyUpdate(update_requested)を送信した場合は、それぞれがupdate_not_requestedで応答し、暗号鍵は2世代分進められることになる
    • KeyUpdateメッセージは古い暗号鍵で暗号化される
  • ハンドシェイクの完全性を検証するためのfinished_key

RFC8446 TLS1.3 4.4.4. Finished
   finished_key =
       HKDF-Expand-Label(BaseKey, 
              "finished", "", Hash.length)

      struct {
          opaque verify_data[Hash.length];
      } Finished;

      verify_data =
          HMAC(finished_key,
               Transcript-Hash(Handshake Context,
                               Certificate*, CertificateVerify*))
  • 現在のトラフィックの鍵をBaseKeyと呼ぶ
    • server_handshake_traffic_secretまたはclient_handshake_traffic_secretまたはclient_application_traffic_secret_N
KIDANI AkitoKIDANI Akito

A.6 拡張

  • TLS拡張はTLV(tag-length-value)構造

  • TLS1.0で非公式に導入、TLS1.1で公式な仕様に
  • TLS1.3ではHandshakeプロトコルのフィールドの多くが拡張に移動
    • 例) supported_versions拡張でのTLSバージョンのネゴシエーション
    • フィールドが固定されているとプロトコルの進化を妨げるため拡張を利用
  • 拡張はTLS1.3のRFC以外にも散らばっている
    • IANAのサイトに拡張の一覧がある
      • 2023年3月時点では約80
    • TLS1.3以外のRFCで定義されているものの例
RFC 拡張名 目的
RFC7301 ALPN TLSハンドシェイク後に利用されるアプリケーションプロトコルをネゴシエートする
RFC6962 CT Certificate Transparencyで利用されるログを送信する
RFC8701 GREASE 不寛容な実装を事前に発見するためにプロトコルの値をランダムに注入する
RFC6066 OCSP 証明書の失効状態のリクエスト、レスポンスに利用
RFC7685 Padding ハンドシェイクの失敗を引き起こすF5製ロードバランサのバグ対策
RFC6066 Sever Name Indication 接続したいサーバのホスト名を示す
KIDANI AkitoKIDANI Akito

A.7 暗号スイート

  • TLS1.3の暗号スイートは5つのみ
    • 旧バージョンでは300以上の暗号スイートが存在
暗号スイート名 AEADアルゴリズム HKDFハッシュ 特徴
TLS_AES_128_CCM_SHA256 AES-128-CCM SHA256 IoTなど制約が厳しいプラットフォーム向け。
TLS_AES_128_CCM_8_SHA256 AES-128-CCM-8 SHA256 IoT向けで上のスイートよりさらに帯域要求が緩い
TLS_AES_128_GCM_SHA256 AES-128-GCM SHA256 唯一実装必須。多くのコンピュータで高速に動作
TLS_AES_256_GCM_SHA384 AES-256-GCM SHA384 量子コンピュータ対策など安全性にマージンを持たせたバージョン
TLS_CHACHA20_POLY1305_SHA256 ChaCha20-Poly1305 SHA256 モバイル端末などAES-GCMのハードウェアアクセラレーションがないプラットフォーム向け
  • AES-CCM系暗号スイート:RFC6655で定義[1]
  • TLS1.3で全ての暗号スイートがPFS(前方秘匿性)に対応し、認証付き暗号(AEAD)を利用
  • 今後暗号スイートの数は増える可能性がある[2]
RFC8446 TLS1.3 B.4. Cipher Suites
      CipherSuite TLS_AEAD_HASH = VALUE;

      +-----------+------------------------------------------------+
      | Component | Contents                                       |
      +-----------+------------------------------------------------+
      | TLS       | "TLS"文字列                              |
      |           |                                                |
      | AEAD      | TLSレコード保護のためのAEADアルゴリズム  |
      |           |                                                |
      | HASH      | HKDFで利用するハッシュアルゴリズム       |
      |           |                                                |
      | VALUE     | 2バイトのID |
      +-----------+------------------------------------------------+
AEADアルゴリズムの詳細
      AES_128_GCM
        ↑     ↑     ↑
アルゴリズム 強度 モード
  • 認証と鍵交換がプロトコルそのものに組み込まれたため、暗号スイートからは削除された
脚注
  1. 2012年7月策定。CCMはCounter with CBC-MACモード。暗号化のためにCounterモードを、認証のためにCBC-MACモードを組み合わせて利用する。 ↩︎

  2. 耐量子暗号アルゴリズムの剪定が進んでいる。また、2022年にはAEGIS系の暗号スイートのドラフトが提出されている。他にも、中国の暗号アルゴリズムShangMiの暗号スイート(2021年のRFC8998)や、ロシアの暗号アルゴリズムGOSTの暗号スイート(2023年のRFC9367が存在する。また、産業用機械間の通信など機密性を必要としないユースケースのためのTLS_SHA256_SHA256などの暗号スイートもある(RFC9150)。 ↩︎

KIDANI AkitoKIDANI Akito

A.8 0-RTT

  • TLS1.3の新機能:TCPと同じ遅延で暗号文通信
    • zero round trip time
    • 当初のSSL/TLS設計では2RTT必要なので遠距離では遅い
    • TLS1.3ではデフォルト1RTT、オプションで0RTT(ただしセキュリティは後退)
KIDANI AkitoKIDANI Akito

A.8.1 実装の詳細

  • 0-RTTは事前共有鍵を用いた認証で実現
    • ハンドシェイク完了後にサーバが0-RTTを受け入れる場合、発行するチケットにearly_data拡張追加
  • クライアントが0-RTTを利用する場合
    • チケットに対応する事前共有鍵をClientHelloに指定(参照:暗号に関するネゴシエーション
    • ClientHelloにearly_data拡張を空で追加
    • 事前共有鍵から導出した鍵で暗号化したearly dataをApplication Dataプロトコルで送信
  • サーバの動作
    • early dataをアプリケーション層に渡して処理可能
    • early dataを無視して通常のハンドシェイクを開始することも可能
  • 原理的な課題
    • PFSの欠如
    • リプレイ攻撃の可能性

A.8.2 0-RTTとPFS

  • 0-RTTを利用しない場合の事前共有鍵はpsk_dhe_keにPFSがあるので安全
  • 0-RTTを利用する場合
    • サーバからの入力がないのでDH鍵交換を使えない
    • PFSが有効になる条件:データの暗号化に利用される鍵が長期間利用されないこと
      • サーバがハンドシェイクのたびに短命の新しいチケット発行が必要
        • クライアントが新しいチケットを優先して使うことも必要
      • チケット鍵の短期間ローテーションが必要
    • 0-RTTでも安全にできるものの、検証可能性がなくなってしまった
      • 利用する場合は実装や設定を確認する必要がある

A.8.3 0-RTTとリプレイ攻撃

  • 0-RTTはリプレイ攻撃に脆弱
    • サーバの乱数が関与しないため、能動的攻撃者がリクエストを複数回送信してもサーバが受け入れてしまう可能性がある
  • TLS1.3のリプレイ攻撃対応
    • obfuscated_ticket_age:能動的攻撃者が無制限に再送信できないようにする
    • early_dataをサーバで記録しておき、再利用のリクエストを弾く
      • 複数台クラスタの場合とても複雑になり厄介...

A.8.4 0-RTTは安全か

  • どれだけの労力を割けるか次第
  • セキュリティを犠牲にしてパフォーマンスを向上する点を認識しておくべき
    • 能動的ネットワーク攻撃なので攻撃には多大なリソースが必要で簡単ではないが。
  • 0-RTTは主にブラウザ向け
    • GETでのみ利用し、POSTやDELETEでは利用しない形にすると多少安全だが、以下の課題も。
      • アプリケーションがGETで副作用を伴う場合には問題
      • CookieやBasic認証などの認証情報がearly dataでPFSなしの暗号化を受ける
KIDANI AkitoKIDANI Akito

A.9 まとめ

  • TLS1.3はこれまでと全く新しいプロトコル
    • ネットワーク遅延の削減:1-RTTが基本、0-RTTも可
    • PFS:全ての暗号スイートが対応、セッションリザンプション含む。0-RTTを除く
    • 平文情報の削減:ハンドシェイク、証明書なども暗号化。
      • 唯一保護されていないSNI(ホスト名) についても対策中
    • 暗号要素技術の刷新:X25519やChacha20へ対応、標準のHKDF採用、etc
このスクラップは2023/04/23にクローズされました