あどけない話

Internet technologies

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"を付けると見れるようになると教えてもらった。そのうち解決されるだろう。

TLS 1.3 開発日記 その15 RSAPSSとX25519

開発日記 その8に書いた「拡張の再利用」の問題に進展があったので、記録しておく。

これまでの方針

拡張が再利用されていても、TLS 1.2 と TLS 1.3 では異なる拡張として扱う。

生じた問題

SignatureSchemeに対する問題:

  • TLS 1.3 クライアントが SignatureScheme に RSA PSS SHA256 を含める
  • サーバは TLS 1.2 を選んでいるのに RSA PSS SHA256 を選択する
  • TLS 1.3 クライアントの TLS 1.2 用の部分は RSA PSS SHA256 に対応していないのでエラーが発生

OpenSSL サーバがこのような挙動をする。

NamedGroupに対する問題:

  • TLS 1.3 クライアントが NamedGroup に X25519 を含める
  • サーバは TLS 1.2 を選んでいるのに X25519 を選択する
  • TLS 1.3 クライアントの TLS 1.2 用の部分は X25519 に対応していないのでエラーが発生

www.google.com がこのような挙動をする。

対処

SignatureSchemeに関しては、TLS 1.3 のドラフトには以下のように書かれている。

Implementations that advertise support for RSASSA-PSS (which is mandatory in TLS 1.3), MUST be prepared to accept a signature using that scheme even when TLS 1.2 is negotiated. In TLS 1.2, RSASSA-PSS is used with RSA cipher suites.

NamedGroupに関しては、RFC 4492 bis で、X25519 と X448 が追加されている。

つまり、このオプションを独立に扱うのは筋が悪い。TLS 1.3 と TLS 1.2 の両方から、同じ拡張のコードを使うべき。

というわけで、まず Haskell tls ライブラリの master ブランチに、RSA PSS SHA256 たちと X25519 たちをサポートする pull request を出した。
TLS 1.3 ブランチは、マージされた後に rebase する(たぶん大変)。

TLS 1.3 開発日記 その14 TLS 1.3 ID19

IETF 98 Chicago の Hackathon に向けて、Haskell TLS ライブラリを TLS 1.3 ID19 に対応させた話。僕は Hackathon には遠隔参加。

ID19に一番乗りしたのは OpenSSL。辻川さんがテストサーバを上げてくれた。

Full と PSK

  • Add pre-extract Derive-Secret stages to key schedule

7.1節の key schedule が変わったので、変更すると OpenSSL と full & PSK ハンドシェイクができた。

0RTT

  • Consolidate "ticket_early_data_info" and "early_data" into a single extension
  • Change end_of_early_data to be a handshake message

ticket_early_data_info と early_data 拡張を1つの拡張とし、end_of_early_data をアラートからハンドシェイクへ変更する。ID18 では、end_of_early_data は early_data を送った直後に送ってよかったが、ID19 からは Server Finished を受け取ってから送る。このせいでコードが汚くなった。

OpenSSLサーバとHaskellクライアントの間で0RTTができないので、原因を調べたところ2つの問題があった。

  • そもそもID18でも0RTTできなかった。OpenSSLでは、チケットの検証が厳しくこれにひっかかっていたので、Haskell側を修正。これでID18で、0RTTできるようになった。
  • ID19 の仕様では Client Finished の計算に EndOfEarlyData を含めないように書かれていた。Haskellの実装はそれに忠実。OpenSSLの実装は EndOfEarlyData を計算に含めていた。EndOfEarlyDataを計算に含めないと、EndOfEarlyDataを守れないので、ハンドシェイクに変更した意味がない。結局、仕様のバグであると合意に至り、Haskell を修正後、OpenSSLと0RTTできるようになった。

HRR

  • Hash ClientHello1 in the transcript when HRR is used. This reduces the state that needs to be carried in cookies.

ID18 ではハンドシェイクメッセージをその都度ハッシュにくべると transcript hashが計算できたが、ID19からは最初のClientHelloのハッシュ値をハッシュにくべなければならなくなった。面倒であったが、結果的には簡潔に実装できた。

まとめ

ID19の仕様のバグも発見したし、上げたテストサーバやクライアントバイナリも利用されたようなので、役に立ってよかった。