あどけない話

Internet technologies

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 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 のソース:

TLS 1.3 開発日記 その10 NSSサーバ

TLS 1.3 のテスト用に公開されているNSSサーバは2つあります。

Haskell TLS 1.3 client では、前者とフルハンドシェイクできるのですが、後者は handshake error を返してきます。一方で、Firefox Nightly や Chrome Canary は、後者に問題なくアクセスできます。

必要な拡張が足らないのかと思い、Firefox Nightly が付けている拡張をすべて付けてみたりしましたがダメでした。この相互接続性は長い間の課題だったのですが、他の作業が落ち着いたこともあり、重い腰を上げて真面目に解析してみました。

採った方法は、Firefox Nightlyが出力する Client Hello のバイト列をそのまま送りつけるユーティリティを作り、徐々に拡張などを削っていて、handshake error を返す要因を特定することです。

驚くべきことにTLS 1.3に関係のない拡張を全部削除しても、tls13はServer Hello を返してきました。そこで、TLS 1.3に必須の拡張から値を徐々に消していきました。そして、Signature Scheme から ecdsa_secp384r1_sha384 を消したときに、handshake error になることを突き止めました。

tls13のサーバ証明書は ECDSAだったのです!同じ NSSサーバでも、franziskuskieferではうまくいくのに tls13ではうまくいかない理由が氷解しました。そこで、Haskell TLS 1.3 にECDSAのコードを入れたところ、めでたく tls13とハンドシェクできるようになりました。

NSSサーバは、NewSessionTicketを送ってきませんので、PSKハンドシェイクなどはできないようです。

追記:

Mozillaの人達と話したら、tls13 が NewSessionTicket を返さないのは、現在のNSSの実装では、ECDSA の証明書に対しそれを返せないからだそうです。証明書を RSA に変えて、NewSessionTicket を有効にしていただきました。めでたく、NewSessionTicket は届くようになりましたが、まだ PSK のハンドシェイクに成功していません。

これは mod_nss の問題のようです。ローカルでビルドした NSS とは、4つのハンドシェイク全部でつながることを確認できました。

TLS 1.3 開発日記 その9 NewSessionTicket

kazuho さんと議論したメモ:

サーバが送るNewSessionTicketは、当然セッションチケットを入れないければならない。以下の構造では ticket がそれにあたる:

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

この ticket は、キースケジュールの最後に出てきた resumption_secret をサーバ自身だけが復号化できるように暗号化したものである。resumption_secret を算出するには、Client Finished が必要である。

というわけで、普通に考えるとNewSessionTicketを送れるのは、Client Finishedを受け取った後になる。しかし、Client Finishedは予測できるので、Server Finished を送った直後に、予測を元に生成した NewSessionTicket を送ることもできる。

サーバは、どちらのタイミングで NewSessionTicket を送るべきだろうか?

TLS 1.3 の API として、以下の3つが用意されているとしよう:

  • handshake()
  • sendAppData()
  • recvAppData()

サーバが handshake() を呼ぶ場合、Client Finished の到着を待つのではなく、Server Finished を送った直後に戻って来てほしい。なぜなら、メール系のプロトコルでは、サーバから先に greeting を送るからだ。greeting は 0.5 RTT で送りたい。

Web系のサーバでは、クライアントから先にデータがやってくるので、handshake() の後にすぐに recvAppData() を呼ぶだろう。recvAppData() は、Client Finished を受信して検証した後、アプリケーションのデータを受信して戻る。

recvAppData()は、データを受信することだけが期待されており、何かを送信するとユーザが驚くと思われる。だから、recvAppData() は NewSessionTicket を送ってはいけない。

そいうわけで、送受信できる handshake() が予測を元にNewSessionTicketをあらかじめ送るのが筋がよさそうに思える。

TLS 1.3 開発日記 その8 開発メモ

これは、http2 Advent Calendar 2016の25日目の記事です。

この記事では、HaskellTLS 1.3を開発した際に難しかった点をまとめます。自分のための覚書です。TLS 1.3のみをフルスクラッチで書くと、そこまで難しくないのかもしれませんが、TLS 1.2以前と共存させるのは大変です。

足らない部品

TLS 1.2のコードが存在しても、TLS 1.3では TLS 1.2で利用されてない部品が必要です:

  • RSA PSS
    • PSSPKCS#1 は異なるパディング方式の署名。RSAの公開鍵/秘密鍵自体は流用できる
  • HKDF
  • X25519 と X448

これらは、Haskell の cryptonite にすべて揃っていたので、少し(かなり?)手を入れるだけで利用できるようになりました。

拡張の再利用

TLS 1.3では、TLS 1.2 の2つの拡張を再利用しています:

  • SignatureAndHashAlgorithm を SignatureSchemeList として再利用
  • EllipticCurveList を NamedGroupList として再利用

当初は、TLS 1.2のパーサ/シリアライザを TLS 1.3 用に書き換えていました。しかし、SignatureAndHashAlgorithmの値などは、コンフィグで設定できるようにすでにユーザに公開しています。TLS 1.3の仕様で書き換えてしまうと、ユーザの意図通りにパラメータが指定されなくなってしまいます。

随分考えましたが、結局単に拡張IDがたまたま同じだけの別の拡張だと割り切ることにしました。TLS 1.2 用のパーサ/シリアライザはまったく変更せず、TLS 1.3 用のパーサ/シリアライザを追加しました。

Client Hello の生成では、ユーザの指定に TLS 1.3 が含まれている場合は TLS 1.3用を、そうでなければ TLS 1.2用を使います。Client Hello の解釈では、TLS 1.3 を選んだ場合にTLS 1.3用を、そうでなければ TLS 1.2用を使います。

この方針で、これまでの挙動を変えることなしに、コードをすっきりさせることができました。

Server Helloの解釈

Client Hello は、TLS 1.2 と TLS 1.3 で書式が(ほぼ)同じです。ですので、Client Hello の解釈では、まずパースした後に分岐できます。

しかし、TLS 1.2 と TLS 1.3 では、Server Hello の書式が異なります。よって、パーサの中で分岐が必要です。Haskellのクライアントでは、パーサを利用する関数が、パーサ内で分岐することを考えて作られていません。

関数プログラミング的に分岐を実現するには、たくさんの関数を書き換える必要がありました。この方法は、やりたいことに比べてコストが高過ぎると判断し、採用しませんでした。結局、パーサが TLS 1.3 だと判断した場合は、命令プログラミング的に例外を挙げてパーサを利用する関数から飛び出し、TLS 1.3 へ分岐することにしました。

メッセージによって構造が異なる拡張

TLS 1.2 ではなかったことですが、TLS 1.3の拡張の中には、ハンドシェイクのメッセージによって構造が異なるものがあります。たとえば、KeyShare 拡張です。

struct {
  select (Handshake.msg_type) {
    case client_hello: KeyShareEntry client_shares<0..2^16-1>;
    case hello_retry_request: NamedGroup selected_group;
    case server_hello: KeyShareEntry server_share;
  };
} KeyShare;

これを表現する直和型のデータ構造を用意するとしましょう。シリアライザは、直和型のタグを見て符号化を変えればよいだけです。問題は、パーサです。パーサは、ハンドシェイクメッセージの型を見ないと正しくパースできません。

これは、関数プログラミング的には、拡張用のすべてのパーサに引数を増やさなければならないことを意味しています。幸運にも、Haskell tls ライブラリには、role (サーバかクライアントか)という引数がすでに渡されていたので、ここをハンドシェイクのメッセージの型に変更することで対応できました。

Google grease

プロトコルのバージョンアップは、いつも大変です。(IPv6関係者の発言なので、説得力があるでしょう?) TLS の場合、生命線は拡張です。拡張のパーサが、知らない値を無視する、つまり、エラーにしないように作ってあれば、将来のバージョンアップへの道が担保されることになります。

将来知らない値が来たときに試すのではなく、いつも知らない値を送ることで、パーサをテストし続けるというのが Applying GREASE to TLS Extensibility のアイディアです。実際、Chrome Canaryは、この grease を送って来ます。

僕が拡張のパーサを最初に書いたときは、知らない値を無視すべきだとは分かってはいたのですが、面倒なので fixme マークを付けながら、エラーにしていました。

僕が実装した Haskell TLS サーバは、すぐに Firefox Nightly とはお話できるようになりました。しかし、TLS 1.3 ID 18 をサポートしたという Canary は、いくら最新をダウンロードしても supported_versions 拡張を送ってこないのです。

しばらくの間、Canary に TLS 1.3 ID 18 を喋らせる方法がまったく分かりませんでした。ある時、念のため tcpdump でパケットをキャプチャしてみたところ、Client Hello に supported_versions が存在するではありませんか。Canary は設定通りに TLS 1.3 ID 18 を喋っていたのです。

そのキャプチャを見た瞬間、grease という言葉が頭を駆け巡りました。はい、悪いのは僕なんです。パーサを直した途端に、Canary と TLS 1.3 で通信できるようになりました。

この話にはオチがあります。後日 grease の提唱者からメールが来ました。どうやら僕の commit log を見たらしく、「grease の効果を調査しているんだけど、君のコードで役に立ったのなら、具体的な話を教えて欲しい」と書かれていました。もちろん、「とっても役に立った」と即答しました。

僕の実装は、差分を最小にするために現在では一つの大きなパッチになっています(git rebase -i + stash したという意味)。しかし、上記の commit log は残したかったので、古い実装は tls13-old というブランチに保存してあります。