あどけない話

Internet technologies

TLS 1.3 開発日記 その21 TLS 1.3 ID22

TLS 1.3 ID21までの仕様は、

  • ServerHelloの書式がTLS 1.2と異なる
  • ChangeCipherSpecがない

という特徴があった。

ServerHelloが異なるということは、TLS 1.3に非対応であったWiresharkで表示できなくて辛いとか、パーサーの中で分岐しなければならないので関数プログラミングの考えでコードが書きにくいなどの問題があった。

その後、はやり世の中にはTLS 1.3を遮断するミドルボックスが少数ながらも存在することが分かり、これらのミドルボックスを騙すための方法が考案された。現在 PR1091 で議論中だが、開発者の間ではこのPRをマージした版がID22とすることで合意が取れている。その方法とはこうだ:

  • ServerHelloの書式をTLS 1.2と同じにする
    • バージョンはTLS 1.2を指定する
    • 圧縮方式が復活。常に0
    • セッションIDが復活。互換性モードの場合、クライアントが指定したセッションIDをコピーする
    • サーバーが選択したバージョンは、supported_versions拡張でクライアントへ伝える
  • HelloRetryRequestを廃止する
    • ServerHelloを利用し、Randomに特定の値を持つServerHelloをHelloRetryRequestとみなす
  • レコードのバージョンは、TLS 1.2とする
  • ChangeCipherSpecを復活させる
    • ChangeCipherSpecを受け取ったら単に無視する
    • 互換モードの場合、適切なタイミングでChangeCipherSpecを送ってよい

確かに、これならミドルボックスを騙せそうだ。というか、初めからこうしておけばよかったのに。

例によって、IETFHackathon前に、OpenSSLとHaskell tlsがこの仕様をサポートし、ある程度の相互互換性を確かめた後、11月11日と12日に開催されたIETF 100で他の実装が追いついて来るという流れになった。僕は日本から遠隔参加するために、サーバーを立ち上げたり、クライアントのバイナリを作成したりした。

IETF 1.3のHackathonでは、遠隔参加が活発だった TLS 1.3チームが "best remote participant" 賞を頂いたそうだ。景品は、IETFに参加しているTLS 1.3チームのメンバーが受け取ったとこのこと。遠隔参加賞なのにぃ!

PatternSynonymsのススメ

PatternSynonymsは、その名の通り、パターンの別名である。GHC 7.8.1 で導入された。GHC 7系のPatternSynonymsは、モジュール内に閉じて入れば何の問題もなかったが、モジュールの外へexportする際は、patternキーワードが必要であり、構成子らしくなかった。

{-# LANGUAGE PatternSynonyms #-}

module A (Foo, pattern Zero) where

newtype Foo = Foo Int

pattern Zero :: Foo
pattern Zero = Foo 0

GHC 8 からは、patternキーワードが不要となり、構成子らしくなった。

{-# LANGUAGE PatternSynonyms #-}

module A (Foo(Zero)) where

newtype Foo = Foo Int

pattern Zero :: Foo
pattern Zero = Foo 0

PatternSynonymsの使いどころ

(ビット幅が決まっている)数値が何らかの意味を持つような問題を考える。たとえば、

0 A
1 B
2 C
その他 予約

のような対応があった場合、Haskell では以下のようなコードを書くことが多いだろう。

data Foo = A | B | C deriving (Show,Eq,Ord,Enum,Bounded)

簡潔でいいのだが、このコードには以下のような問題がある。

  • 「その他」の数値が発生しうる場合にはどうするのか?

以下のように構成子を増やすと、Enum を導出できなくなる。

data Foo = A | B | C | Other Int deriving (Show,Eq,Ord)

他にも問題がある。

  • 値が0から始まらない場合どうするのか?
  • 値が連続してない場合どうするのか?

という訳で、この方法は筋が悪い。そこで、PatternSynonymsの登場である。

module A (Foo(A,B,C),fromFoo,toFoo) where

newtype Foo = Foo {
    fromFoo :: Int
  } deriving (Eq)

toFoo :: Int -> Foo
toFoo = Foo

pattern A :: Foo
pattern A = Foo 0

pattern B :: Foo
pattern B = Foo 1

pattern C :: Foo
pattern C = Foo 2

instance Show Foo where
    show A = "A"
    show B = "B"
    show C = "C"
    show x = "Foo " ++ (show $ fromFoo x)

少しコード量は増えるが、問題に柔軟に対応できるのでストレスがない。

TLS 1.3 開発日記 その20 TLS 1.3 ID21

TLS 1.3 ID21に追従した。

  • Add a per-ticket nonce so that each ticket is associated with a different PSK.

NewSessionTicket に ticket_nounce が増えた。

struct {
    uint32 ticket_lifetime;
    uint32 ticket_age_add;
    opaque ticket_nonce<1..255>;
    opaque ticket<1..2^16-1>;
    Extension extensions<0..2^16-2>;
} NewSessionTicket;

ID 20 までは、key schedule でいうところの PSK の値は、resumption_master_secret そのものだった。ID 21 では、以下のように算出するようになった。

PSK = HKDF-Expand-Label(resumption_master_secret, "resumption", ticket_nonce, Hash.length)

セッションで複数回NewSessionTicketが発行される場合は、ticket_nounceはユニークな値でなければならない。それを守れば、チケットごとにユニークなPSKの値が生成される。ID 20の仕様に対する明らかな攻撃方法は発見されていないが、ID 21の方が直感的に安全だろうと思われている。

セッションで1回しかNewSessionTicketが発行されない場合は、固定の文字列でもよい。(空文字列も許容するよう使用を変更すべき?)

なお、ID 21の HKDF-Expand-Label は曖昧になってしまったので、そのまま実装すると他の実装とは通信できない。具体的には、HKDF-Expand-Label は以下のように定められている。

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

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

ID 20 までは、HashValue の部分は、本当にハッシュ値か空文字列しか取らなかった。空文字列は、特殊なハッシュ値だと解釈されていた。HashValue の長さと、HKDF-Expand-Label の出力の長さを表す Length は、たまたま一緒であった。

今回、ticket_nounce も取るようになったので、その前提が崩れたことを OpenSLL の担当者が発見した。OpenSSLでは、上記の仕様を以下のように解釈して、実装している。

HKDF-Expand-Label(Secret, Label, Value, OutputLength) = HKDF-Expand(Secret, HkdfLabel, OutputLength)

struct {
    uint16 length = OutputLength;
    opaque label<7..255> = "tls13 " + Label; // フィールドの長さが先頭に付く
    opaque value<0..255> = Value; // フィールドの長さが先頭に付く
} HkdfLabel;

picotls と Haskell tls も、OpenSSLの解釈を採用している。

TLS 1.3 開発日記 その19 OpenSSL

OpenSSLで、TLS 1.3を使う方法の覚書き。以下が参考になる。

ビルド

OpenSSL が現在サポートしているのは draft 20。そのソースの取り出し方はこう:

% git clone https://github.com/openssl/openssl.git

(修正:)draft 22のソースの取り出し方はこう:

% cd openssl
% git checkout -t origin/tls1_3-draft-22

ビルドの仕方はこう:

% ./config enable-tls1_3
% make
% make test

OpenSSL サーバ

s_server の使い方:

% cd util
% ./opensslwrap.sh s_server -accept 13443 -www -key $SOMEWHERE/key.pem -cert $SOMEWHERE/certificate.pem -curves X25519:P-256
  • ORTT を受けるには -early_data が必要。現時点で、-www とは両立しない。
% ./opensslwrap.sh s_server -accept 13443 -early_data -key $SOMEWHERE/key.pem -cert $SOMEWHERE/certificate.pem -curves X25519:P-256

OpenSSL クライアント

Full nego:

./opensslwrap.sh s_client -debug -connect 127.0.0.1:13443

HRR:

  • curves の先頭が keyshare となる。
./opensslwrap.sh s_client -debug -connect 127.0.0.1:13443 -curves P-521:P-256

PSK:

./opensslwrap.sh s_client -debug -connect 127.0.0.1:13443 -sess_out ticket
./opensslwrap.sh s_client -debug -connect 127.0.0.1:13443 -sess_in ticket

0RTT:

./opensslwrap.sh s_client -debug -connect 127.0.0.1:13443 -sess_out ticket
./opensslwrap.sh s_client -debug -connect 127.0.0.1:13443 -sess_in ticket -early_data $SOMEWHERE/early-data.txt

TLS 1.3 開発日記 その18 TLS 1.3 ID20

TLS 1.3 ID20に追従した。

やったのは、これだけ。

  • Shorten labels for HKDF-Expand-Label so that we can fit within one compression block

つまり、ラベルの文字列を変えただけ。現在、OpenSSLと相互試験中。

ちなみに、Haskell tls ライブラリの TLS 1.3 対応状況は、issue 167 にまとめてある。

追記:

サーバが early data を受け入れなかった場合、EndOfEarlyData は送ってはならず、従って Client Finished の計算に含めてはならない。

TLS 1.3 開発日記 その17 AEAD

TLS 1.2とTLS 1.3のAEAD の違いについて、AEADの一つであるAES 128 GCMを例にとって説明する。

TLS 1.2のAEAD

以下の3つのRFCをよーく読まないといけない。

  • RFC 5246: The Transport Layer Security (TLS) Protocol Version 1.2
  • RFC 5116: An Interface and Algorithms for Authenticated Encryption
  • RFC 5288: AES Galois Counter Mode (GCM) Cipher Suites for TLS

RFC 5246 の 6.2.3.3 節では、AEADで暗号化されたレコードが以下のように定義してある。

struct {
    opaque nonce_explicit[SecurityParameters.record_iv_length];
    aead-ciphered struct {
        opaque content[TLSCompressed.length];
    };
} GenericAEADCipher;

aead-ciphered の部分は、以下の関数で生成される。

AEADEncrypted = AEAD-Encrypt(write_key, nonce, plaintext, additional_data)

賢明な読者なら「authentication tag はどこにいった?」と疑問に思うだろう。RFC 5116の5.1節には、こう書いてある。

The AEAD_AES_128_GCM ciphertext is formed by appending the authentication tag provided as an output to the GCM encryption operation to the ciphertext that is output by that operation.

つまり、AEAD-Encryptの中身はこんな感じ:

AEAD-Encrypt(write_key, nonce, plaintext, additional_data) {
    (cipher, auth_tag) = aead-encrypt(write_key, nonce, plaintext, additional_data);
    return (cipher + auth_tag);
}

一方、AEAD-Decryptのインターフェイスは、こう定義してある:

TLSCompressed.fragment = AEAD-Decrypt(write_key, nonce, AEADEncrypted, additional_data)

中身はこんな感じになる:

AEAD-Decrypt(write_key, nonce, AEADEncrypted, additional_data) {
    (cipher, auth_tag1) = split(AEADEncrypted);
    (plaintext, auth_tag2) = aead_decrypt(write_key, nonce, cipher, additional_data);
    if (auth_tag1 != auth_tag2) throw_error();
    return plaintext;
}

引数のadditonal_dataも、RFC 5246に定義してある:

additional_data = seq_num + TLSCompressed.type + TLSCompressed.version + TLSCompressed.length;

この後半の3つであるが、TLSCompressedレコードの定義をみると、なんのことはない、レコードヘッダだと分かるだろう。

struct {
    ContentType type;       /* same as TLSPlaintext.type */
    ProtocolVersion version;/* same as TLSPlaintext.version */
    uint16 length;
    opaque fragment[TLSCompressed.length];
} TLSCompressed;

つまり、こういうことである。

additional_data = seq_num + レコードのヘッダ

最後は nonce。RFC 5116の5.1節には、"N_MIN and N_MAX are both 12 octets" と書いてある。最小値も最大値も12バイトだから、nonceは12バイトであると分かる。RFC 5288は、こう書かれている:

          struct {
                opaque salt[4];
                opaque nonce_explicit[8];
             } GCMNonce;

   The salt is the "implicit" part of the nonce and is not sent in the
   packet.  Instead, the salt is generated as part of the handshake
   process: it is either the client_write_IV (when the client is
   sending) or the server_write_IV (when the server is sending).  The
   salt length (SecurityParameters.fixed_iv_length) is 4 octets.

というわけで、4バイトのsaltにはclient_write_IVかserver_write_IVを入れればよい。nonce_explicitについては:

The nonce_explicit MAY be the 64-bit sequence number.

なので、seq_numでよいと分かる。

TLS 1.2 を実装するの、嫌になったでしょ?

TLS 1.3のAEAD

TLS 1.3のAEADを理解するには、ドラフトを読めばよい。

the additional data input is empty (zero length)

というわけで、additional data には空文字列を渡す。

nonce は、

  • The 64-bit record sequence number is encoded in network byte order and padded to the left with zeros to iv_length.
  • The padded sequence number is XORed with the static client_write_iv or server_write_iv, depending on the role.

のように作ればよい。

TLS 1.3 開発日記 その16 Wireshark

Wiresharkはv2.3.0からTLS 1.3 draft 19に対応する。めでたい。すぐに使いたい人は、Nightlyビルドをとってくるとよい。

追記:v2.5.0rc0-1840-gd35ed012ce から TLS 1.3 draft 22 に対応している。(draft 22 はまだ出てないけど。)

使ってみる

ポート13443で起動しているHaskellサーバとpicotlsクライアントの通信をtcpdumpでキャプチャしたファイルを"pico.pcap"とする。これを表示してみよう。

% tshark -dtcp.port==13443,ssl -Y ssl -r pico.pcap -V
Secure Sockets Layer
    TLSv1 Record Layer: Handshake Protocol: Client Hello
        Content Type: Handshake (22)
        Version: TLS 1.0 (0x0301)
        Length: 178
        Handshake Protocol: Client Hello
            Handshake Type: Client Hello (1)
            Length: 174
            Version: TLS 1.2 (0x0303)
            Random: c61e4f47e9d8f0c6e713bd872bd488c1a8c9bb855b8ccb4a...
                GMT Unix Time: May  1, 2075 03:39:03.000000000 JST
                Random Bytes: e9d8f0c6e713bd872bd488c1a8c9bb855b8ccb4a2bfad94d...
            Session ID Length: 0
            Cipher Suites Length: 2
            Cipher Suites (1 suite)
                Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
            Compression Methods Length: 1
            Compression Methods (1 method)
                Compression Method: null (0)
...
            Extension: key_share (len=71)
                Type: key_share (40)
                Length: 71
                Key Share extension
                    Client Key Share Length: 69
                    Key Share Entry: Group: secp256r1, Key Exchange length: 65
                        Group: secp256r1 (23)
                        Key Exchange Length: 65
                        Key Exchange: 046ed5a9aca26248b0fc322e218e778ebd17f4b47add1a7a...

ClientHello のkey_shareが表示できてる。やったね!

オプションの意味:

  • "-dtcp.port==13443,ssl": ポート13443をTLSとして解析
  • "-Y ssl": TLSだけを表示
  • "-r pico.pcap": 入力ファイルは "pico.pcap"
  • "-V":詳細表示

復号化する

picotlsでは、cliに"-l"でファイルを指定すると、セッションキーをそのファイルに書き出す。ここでは、ファイル名を"pico.keys"とする。内容はこんな感じ:

% cat pico.keys
SERVER_HANDSHAKE_TRAFFIC_SECRET 秘密の鍵1
CLIENT_HANDSHAKE_TRAFFIC_SECRET 秘密の鍵2
SERVER_TRAFFIC_SECRET_0 秘密の鍵3
CLIENT_TRAFFIC_SECRET_0 秘密の鍵4

このファイルをtsharkに指定すると、暗号化されている部分が復号化できる。

% tshark -ossl.keylog_file:pico.keys -dtcp.port==13443,ssl -Y ssl -r pico.pcap
    4   0.028292 IPアドレスA → IPアドレスB TLSv1 249 Client Hello
    6   0.060786 IPアドレスB → IPアドレスA TLSv1.3 1514 Server Hello, Encrypted Extensions
    7   0.060844 IPアドレスB → IPアドレスA TLSv1.3 1514 Certificate [TCP segment of a reassembled P
DU]
    8   0.060847 IPアドレスB → IPアドレスA TLSv1.3 163 Certificate Verify, Finished
   11   0.062084 IPアドレスA → IPアドレスB TLSv1.3 124 Finished
   12   0.062843 IPアドレスB → IPアドレスA TLSv1.3 368 New Session Ticket
   15   4.281915 IPアドレスA → IPアドレスB TLSv1.3 89 Application Data
   17   4.307195 IPアドレスB → IPアドレスA TLSv1.3 229 Application Data
   19   4.307612 IPアドレスB → IPアドレスA TLSv1.3 90 Alert (Level: Warning, Description: Close Not
ify)

やっほー!

オプションの意味:

  • "-ossl.keylog_file:pico.keys": 鍵のファイルは "pico.keys"

Macでの戦い

しかし、Macでは復号化できなかった。教えてもらった"-ossl.debug_file:ssl-debug.txt "というデバッグオプションを付けてみると:

% cat ssl-debug.txt
...
Libgcrypt version: 1.5.0
...
Libgcrypt is older than 1.6, unable to verify auth tag!

というわけで、libgcryptが古いのが原因だった。"-ossl.ignore_ssl_mac_failed:TRUE"を付けると見れるようになると教えてもらった。そのうち解決されるだろう。