あどけない話

Internet technologies

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

[TLS][Haskell]TLS 1.3 開発日記 その13 TLS 1.3を遮断する中継装置

原因

  • TLS 1.3は、TLS 1.2と区別のつかないClient Helloを送る。拡張でバージョンが1.3だと教える。
  • TLSの実装は、知らない拡張は単に無視してエラーにしてはいけないという鉄則により、TLSのバージョンアップは順調にいくはずだと信じられていた。
  • BlueCoatは、サーバからのCertificateの中身を見て、中継するか否かを決めるらしい。
  • TLS 1.3を知らないBlueCoatには、Client HelloがTLS 1.2に見える。
  • しかし、サーバから戻って来たハンドシェイクは暗号化されているから中身が読めない。
  • TLS 1.2じゃない」と言って、コネクションを遮断。

考察

  • 世の中にはいろんな実装がある。はぁ。
  • Firefoxは、TLS 1.2にフォールバックするらしい。
  • ChromeでもTLS 1.2にフォールバックすれば一応問題解決。
  • しかし、TLS 1.2でも1.3でもうまくいくようにClient Helloは設計されているので、TLS 1.2にフォールバックするのは、なんだかなぁという感じ。
  • BlueCoatよ、早く直して。

TLS 1.3 開発日記 その12 OCSP と SCT

TLS 1.2では Server Hello 拡張であった OCSP と SCT は、TLS 1.3ではハンドシェイクメッセージである Certificate の拡張となった。

OCSP

証明書は有効期限内であっても、失効している可能性がある。失効しているかを調べる伝統的なやり方は、CRL(Certificate Revocation List)であった。

CRLでは、クライアントが失効リストを取って来て、その中に対象があるかないかを調べる。これに対し、OCSPサーバに問い合わせると、証明書が(いつの時点で)有効か教えてくれるのが OCSP(Online Certificate Status Protocol) である。

TLSクライアントが、OCSPサーバに問い合わせるのは現在では非推奨である。その代わり、TLSサーバが定期的に問い合わせ、証明書と一緒にフレッシュな OCSP Response を送ってくるのが、OCSP stapling である。

証明書に対する OCSP サーバの情報は、証明書の中に入っている。Let's ecnrypt で取得した署名書では、以下のような感じ:

% openssl x509 -text -in cert.pem
Certificate:
    Data:
...
            Authority Information Access: 
                OCSP - URI:http://ocsp.int-x3.letsencrypt.org/
                CA Issuers - URI:http://cert.int-x3.letsencrypt.org/
...

OCSPサーバに問い合わせてみる:

% openssl ocsp \
  -noverify \
  -issuer chain.pem -cert cert.pem -CAfile chain.pem \
  -url http://ocsp.int-x3.letsencrypt.org \
  -header Host ocsp.int-x3.letsencrypt.org \
  -resp_text -respout resp.der
OCSP Response Data:
    OCSP Response Status: successful (0x0)
    Response Type: Basic OCSP Response
    Version: 1 (0x0)
    Responder Id: C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
    Produced At: Feb 23 06:37:00 2017 GMT
    Responses:
    Certificate ID:
      Hash Algorithm: sha1
      Issuer Name Hash: 7EE66AE7729AB3FCF8A220646C16A12D6071085D
      Issuer Key Hash: A84A6A63047DDDBAE6D139B7A64565EFF3A8ECA1
      Serial Number: 03AF31CC447C2CEF256FC77839190A70DDE2
    Cert Status: good
    This Update: Feb 23 06:00:00 2017 GMT
    Next Update: Mar  2 06:00:00 2017 GMT
...

便利なラッパーとして fetch-ocsp-responseがある。

SCT

Certificate Transperancyは、(理想的にはすべての)証明書を登録するサービスである。ロガー呼ばれるサーバには、証明書の追加だけができるログを持っている。2つの使い方がある。

  • ある証明書のハッシュを送ると、ログの中に存在しているか確認できる。
  • ロガーから、証明書群の差分を取れる。つまり、ロガーが持っている証明書すべてを取得できるので、不正な証明書が発行されてないか調べられる。たとえば、自分のドメインを名乗る不正なサーバに対する証明書を発見できる。

ロガーには、サーバ名から証明書を検索する機能はない。そういうサービスは、上記二番目の機能を使って外部に作る。たとえば、crt.sh

ロガーは、新規に証明書を登録された場合、SCT (signed certificate timestamp)を返す。これは、ロガーの署名が付いた存在証明である。TLSクライアントは、SCTの署名をロガーの公開鍵で検証すれば、対応する証明書がログに入っていることを確認できる。つまり、ロガーに問い合わせる必要はないし、TLSクライアントがロガーに問い合わせるのは非推奨である。

TLSサーバからTLSクライアントにSCTを渡すには、2つの方法がある。

  • 証明書の拡張に入れる
  • TLS の Server Hello の拡張に入れる

CA から、サーバ管理者に SCT を配布するには、以下の方法がある。

  • 発行する証明書に入れる
  • OCSPの拡張で返す

Let's encrypt の CA は、証明書を発行する際、ct.googleapis.com/pilot と ct.googleapis.com/icarus に証明書を登録するようだ。つまり、Let's encrypt の CA は、SCT を知っているが、残念ながらそれをサーバ管理者には教えてくれない。

  • certbot renew しても SCT ファイルはできない
  • certbot renew して得られる証明書には SCT が入っていない
  • OCSP で問い合わせても SCT 拡張は入っていない

八方塞がりである。

実はロガーは、すでにある証明書が登録されようとした場合も、SCT を返す。この方法を実現してくれるのが、ct-submitである。(もちろん新規登録も可能。)

% ct-submit ct.googleapis.com/pilot < fullchain.pem > sct

TLS 1.3 開発日記 その11 NSS

NSSのビルド

以下を参照:

(変更:)ブランチ:

  • default は draft 22

NSSサーバの動かし方

共有ライブラリを使っている場合は、適当にパスを加える。nssroot ディレクトリで:

% ./dist/$PLATFORM/bin/selfserv -d tests_results/security/localhost.1/ssl_gtests -n rsa -p 13443 -V tls1.3:tls1.3 -u -Z
  • -u が PSK を有効にする
  • -Z が 0RTT を有効にする

ヘルプの表示:

% ./dist/$PLATFORM/bin/selfserv -h

selfserv のソース:

NSSクライアントの動かし方

共有ライブラリを使っている場合は、適当にパスを加える。nssroot ディレクトリで:

ヘルプの表示:

% ./dist/$PLATFORM/bin/tstclnt -\?

フルネゴシエーション

% ./dist/$PLATFORM/bin/tstclnt -D -V tls1.3:tls1.3 -h 127.0.0.1 -p 13443 -o

HRR:

% ./dist/$PLATFORM/bin/tstclnt -D -V tls1.3:tls1.3 -h 127.0.0.1 -p 13443 -o -I P521,x25519

PSK:

% ./dist/$PLATFORM/bin/tstclnt -D -V tls1.3:tls1.3 -h 127.0.0.1 -p 13443 -o -L 2 -A 

入力ファイルの例:

% cat $SOMEWHERE/early-data.txt
GET / HTTP/1.1
Host: 127.0.0.1
Connection: close

0RTT:

% ./dist/$PLATFORM/bin/tstclnt -D -V tls1.3:tls1.3 -h 127.0.0.1 -p 443 -o -L 2 -A $SOMEWHERE/early-data.txt -Z

tstclnt のソース: