Apache mod_http2 で 103 EarlyHints (RFC 8297) を試す

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



20161208 今回はtoy serverを使用しましたが、103 EarlyHintsを送信できるNginxモジュールを書きました
103 EarlyHintsを送信するNginxモジュール書いた


103 Early Hints

103 Early Hintsは、@kazuho氏によって提案されている仕様です。すでに Individual-Draftが提出されています。
https://tools.ietf.org/html/draft-kazuho-early-hints-status-code-00


IETF97でも仕様についての議論が行われており、その際のスライドを見ると分かりやすいかと思います
https://www.ietf.org/proceedings/97/slides/slides-97-httpbis-sessb-early-hints-00.pdf


簡単に言うと、HTTPレスポンスは一般的にコンテンツの生成が終わってからステータスコードが決定します。そのため、特定のHTTPレスポンスヘッダをまず返したい!ということは出来ません。


そこで、informationalステータスコードである100番代の "103" を使用することでHTTPレスポンスヘッダをコンテンツの生成が終わる前にクライアントに通知することが出来ます。一般的に馴染みのない 100番代のステータスコードですが、少々特殊でこのHTTPレスポンスのみ連続で送信する事ができます。但し、正しく実装されていないクライアント・サーバもあるので注意が必要であり、仕様としては議論が続くところかと思います。


IETF97の発表資料の書かれている通り、ユースケースとしてLinkヘッダでPreloadを先行してレスポンスすることで、クライアントは早いタイミングでそのリソースを取得しようとします。
また、プロキシがバックエンドWebサーバより103を受け取った場合、クライアントとHTTP/2で通信していれば サーバプッシュを使用しリソースをプッシュすることもできます。


すでに、h2oやnghttp2で対応されているようです

Apache mod_http2 で試す

mod_http2(nghttp2)でざっと試す

  • Ubuntu 16.04
  • Apache2.4 (Revision 1772437)
  • nghttp2 (commit 85ba33c08f46)
  • Openssl 1.0.2g


特殊なことはないが、nghttp2をインストールしてから、Apache2.4をsvnからチェックアウトしビルドする。その後、http2の有効化およびリバースプロキシの設定を入れる。

# https://github.com/nghttp2/nghttp2 にそって、インストールしておく
sudo apt-get install subversion build-essential autoconf libxml2 libxml2-dev libtool libtool-bin libpcre3-dev

svn checkout http://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x httpd-2.4.x
cd ./httpd-2.4.x/
./buildconf
./configure  --enable-http2 --with-libxml2

make
sudo make install
構成


バックエンドWebサーバとして、toy serverを準備しておく。こいつが、103を返す

proxy

ざっと、ココらへんの設定を入れる。(フロントはhttp2)

#h2の設定
Protocols h2 http/1.1
ProtocolsHonorOrder On
#H2EarlyHints on

#proxy
ProxyStatus On
ProxyPreserveHost On
ProxyPass / balancer://test
<Proxy balancer://test>
        BalancerMember http://127.0.0.1:8080 loadfactor=10
</Proxy>
バックエンド

status 103を返すバックエンドサーバを簡易的に準備。
Linkヘッダでpreloadを指定します。nghttp2は103のpreloadヘッダを解釈し、HTTP/2 Server Pushをしてくれます。

vagrant@vagrant:~$ cat ./res
HTTP/1.1 103
Link: </hoge.css>;rel=preload

HTTP/1.1 200 OK
Date: Sun, 04 Dec 2016 00:00:00 GMT

helloworld


vagrant@vagrant:~$ while true; do ( cat ./res ) | nc -l 8080 >/dev/null; [ $? != 0 ] && break; done &
試してみる

nghttp2で接続を試みる

vagrant@vagrant:~$ nghttp https://localhost -vn --no-dep|lv |grep -i frame
...
[  0.011] send HEADERS frame <length=33, flags=0x05, stream_id=1>
[  0.013] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
[  0.015] recv PUSH_PROMISE frame <length=53, flags=0x04, stream_id=1>
[  0.015] recv HEADERS frame <length=57, flags=0x04, stream_id=1>
[  0.016] recv DATA frame <length=14, flags=0x01, stream_id=1>
[  0.016] recv HEADERS frame <length=55, flags=0x04, stream_id=2>
[  0.016] recv DATA frame <length=528, flags=0x01, stream_id=2>
[  0.016] send GOAWAY frame <length=8, flags=0x00, stream_id=0>

ちゃんと、103のLinkヘッダを読んで、Proxy側からPUSH_PROMISEが送信されていることが確認できる。
(今回のtoy serverは103と200を同時に返しているの意味は薄いが)


また、mod_http2の設定で「H2EarlyHints」をon にするとクライアント側に103が伝達される

vagrant@vagrant:~$ nghttp https://localhost -vn --no-dep|lv |grep -i -e frame  -e status
...
[  0.013] send HEADERS frame <length=33, flags=0x05, stream_id=1>
[  0.014] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
[  0.017] recv PUSH_PROMISE frame <length=53, flags=0x04, stream_id=1>
[  0.017] recv (stream_id=1) :status: 103
[  0.017] recv HEADERS frame <length=41, flags=0x04, stream_id=1>
[  0.017] recv (stream_id=1) :status: 200
[  0.017] recv HEADERS frame <length=57, flags=0x04, stream_id=1>
[  0.017] recv DATA frame <length=14, flags=0x01, stream_id=1>
...

ntpdのLeap Smearingを有効にし、うるう秒を24時間かけて調整する

うるう秒を挿入せず、1秒分を長い時間で少しずつ適応することでソフトウェアなどのバグを回避する Leap Smearingは ntpdでも使用できる。


4.2.8.p3 と 4.3.47以降でサポートしているが、defaultでは無効になっているので自身でビルドする必要がある。

ubuntu 16.04で試す

wget http://www.eecis.udel.edu/~ntp/ntp_spool/ntp4/ntp-4.2/ntp-4.2.8p9.tar.gz
tar zxvf ./ntp-4.2.8p9.tar.gz
cd ./ntp-4.2.8p9/
./configure --enable-leap-smear
make


動作確認(既存のntpdが起動してれば事前に切っておく)
ntpq -c rv で leapsmearinterval が表示される

$ echo 'leapsmearinterval 86400' | sudo tee -a /etc/ntp.conf
$ sudo ./ntpd/ntpd

$ ntpq -c rv
associd=0 status=0618 leap_none, sync_ntp, 1 event, no_sys_peer,
version="ntpd 4.2.8p9@1.3265-o Thu Dec  1 13:54:30 UTC 2016 (1)",
processor="x86_64", system="Linux/4.4.0-31-generic", leap=00, stratum=3,
precision=-23, rootdelay=8.972, rootdisp=292.929, refid=59.106.180.168,
reftime=dbeab436.6e302275  Thu, Dec  1 2016 14:37:42.430,
clock=dbeab49a.75ac45a4  Thu, Dec  1 2016 14:39:22.459, peer=58337, tc=6,
mintc=3, offset=-86.392728, frequency=-125.660, sys_jitter=0.000000,
clk_jitter=28.609, clk_wander=2.963, leapsmearinterval=86400,
leapsmearoffset=0.000

おまけ

現在IETFで「Network Time Protocol Best Current Practices」として、NTPのベスト・プラクティスが書かれている。


section 4.6.1でLeap Smearingについて書かれている。パブリックサーバとして使用しないように勧められている他、現在のサーバ構成を把握しSmearingしてるサーバとSmearingしてないサーバを混在させないようにすることが推奨されている

Nginxのtoken binding実装を試す

20181016追記
Remove support for Token Binding
https://chromium.googlesource.com/chromium/src/+/2243e8002e3025b3f8386c13be7437fc8b597e2a

chromeの実装は削除されました下記も合わせて参照のこと
https://groups.google.com/a/chromium.org/forum/?nomobile=true#!topic/blink-dev/OkdLUyYmY1E

Token Binding

CookieやOAuth2.0のトークンはbearer tokenと呼ばれ、そのトークンを持ってる人であれば使用することができます。Token Bindingは、各TLSコネクション固有の秘密鍵で署名することで、第三者による別のコネクションにおけるそれらの使用を制限できるものです。


仕様及びユースケースについては、IETFで仕様策定中のドキュメントを参照ください

ngx_token_binding

GoogleがNginxのtoken bindingモジュールを公開していたので試す
https://github.com/google/ngx_token_binding

ビルド
# ngx_token_binding 準備
git clone https://github.com/google/ngx_token_binding.git
cd ./ngx_token_binding 
git submodule update --init
cd ../

# openssl に変更を加える
git clone https://github.com/openssl/openssl.git
vim ./openssl/ssl/t1_lib.c

git diff
diff --git a/ssl/t1_lib.c b/ssl/t1_lib.c
index ce728b0..e2e01bb 100644
--- a/ssl/t1_lib.c
+++ b/ssl/t1_lib.c
@@ -2461,7 +2461,7 @@ static int ssl_scan_clienthello_tlsext(SSL *s, CLIENTHELLO_MSG *hello, int *al)
          * callback and record the extension number so that an appropriate
          * ServerHello may be later returned.
          */
-        else if (!s->hit) {
+        else {
             if (custom_ext_parse(s, 1, currext->type,
                     PACKET_data(&currext->data),
                     PACKET_remaining(&currext->data), al) <= 0)

#ビルド
wget https://nginx.org/download/nginx-1.11.6.tar.gz
tar zxvf nginx-1.11.6.tar.gz
cd ./nginx-1.11.6/
./configure --with-http_ssl_module --with-openssl=/home/vagrant/openssl/ --addmodule=/home/vagrant/ngx_token_binding

make
sudo make install


nginx.conf

    server {
        listen       80;
        server_name  localhost;
        add_header set-cookie id=test;
    }
    server {
        listen       443 ssl ;
        server_name  localhost;

        token_binding on;
        token_binding_cookie all;
        token_binding_secret secret;

        ssl_certificate /home/vagrant/server.crt;
        ssl_certificate_key /home/vagrant/server.key;

        location / {
            proxy_pass http://127.0.0.1:80;
        }
    }

試す

ChromeのCanary buildがToken Bindingに対応しているので (chrome://flags/ から Token Bindingを有効にする)、それでNginxに接続する。
今回は単純に、ブラウザと単一のサーバとの通信を行い、CookieにToken Bindingのパラメータがつくことを確認する(ユースケースのFirst-party Use Cases)


ブラウザからサーバに複数回アクセスし、デベロッパーツールを確認する

レスポンスのset-cookie及び、リクエストのcookieに文字列が付与されていることが確認できる。


あわせて、サーバ側のログでは 「id=test」のcookieとして認識されていることも確認できる

tail /var/log/nginx/nginx-https.log
192.168.0.1 - - [24/Nov/2016:16:09:17 +0000] "GET / HTTP/1.1" 200 cookie: id=test 


流れにするとこんな感じ


このCookieはもちろん他のブラウザにセットしても正しく使用することはできない

NTP leap indicatorを上書きするProxyを書いた

前置き


かなり無理矢理かつ、動作を保証するものではありません。
手習いで試しに書いてみたぐらいの温度感です


うるう秒

元旦「うるう秒」でエンジニア悲鳴 「年末年始がなくなる」”と言ったニュースサイトでも取り上げられているように、1月1日 日本時間朝9時にうるう秒が挿入されます。つまり、8時59分60秒 という時間が挿入されます。


60秒が挿入されるとはどういうことか
NTP うるう秒(閏秒)」こちらのサイトの情報を引用させていただくと

日付(JST) NTP time LI
2006/01/01 08:59:59 3345062399 01
2006/01/01 08:59:60 3345062400 01
2006/01/01 09:00:00 3345062400 00

NTPサーバからのレスポンスには事前にうるう秒の挿入を示すLeap Indicatorというフラグがセットされます。そうして、60秒から00秒にかけては同じ時間がレスポンスされて帰ってきます。


このLIですが、このLIがセットされているとSlewモードからStepモードに移行してしまうNTPクライアントのバグなども存在していますし、なかなか厄介な存在です。


前回は、このLIがNTPクライアントに到達しないように、該当時間だけ上位のNTPサーバとの同期を停止するという方法を取ったという人もいるようです。もちろん、60秒を挿入しないぶん1秒早く時刻が刻まれていきますが、殆どのソフトウェアは個々に対応が進んでいるとはいえ予期せぬうるう秒に起因する不具合を踏むよりはマシなのかと思います。

NTP leap indicatorを上書きするProxyを書いた

上位のNTPサーバと同期を止める方法は、ローカルのハードウェアクロックを頼りにしており、それはそれで心もとないなと思い、LIフラグをオフにするUDP Proxyを書いてみました。


unset-leap-indicator-proxy
https://github.com/flano-yuki/unset-leap-indicator-proxy


NTPサーバを指定して起動すると、UDPの123 portでLISTENし、受け取ったNTP Requestを指定したサーバにProxyします。

vagrant@vagrant:~/tmp$ sudo go run ./proxy.go -v ntp.nict.jp
Info: Start Proxy on  :123  to ntp.nict.jp
Received from 192.168.0.179:123
Received from 192.168.0.179:123
Received from 192.168.0.179:123



このProxyはNTPパケットを実際にパースすることはなく、単純にUDPボディの上位2bit(Leap Indicator)を 00 に上書きします。
これによってNTPクライアントにLIが入ることもありません。しかし、うるう秒のタイミングでは60秒が挿入されず1秒ずれてしまうので、そこからはNTPの時刻同期の仕組みによってずれが解消されていくものと思います。


ACL機能がないため踏み台攻撃に注意してください。

CT対応を示すExpect-CTヘッダとは

追記 2021/11/15
Expect-CTヘッダの役割は、CTの利用が必須となり不要となる方向です
asnokaze.hatenablog.com


Certificate Transparency

Certificate Transparencyと呼ばれる、不正な証明書の発行を検知する仕組みがGoogle社によって考案され、RFC 6962として標準化されています。もちろん、Google Chromeもこの機能に対応しており、ログサーバーにSigned Certificate Timestamp(SCT)を検証し、正しく発行された証明書なのかを検証します。


詳しい仕組みについては、各社CAの説明を読むとわかりやすいかと思います。

このCTですが、GoogleChrome teamはCA/Browserにて、2017年10月以降に発行された証明書は信頼されるためにChromeのCTポリシーを尊守することが期待される、とアナウンスをしているようです。
https://groups.google.com/a/chromium.org/forum/#!msg/ct-policy/78N3SMcqUGw/ykIwHXuqAQAJ


ChromeのCTポリシーは、chromiumwikiより確認できます。
https://www.chromium.org/Home/chromium-security/certificate-transparency


また、来月行われるIETF97のHTTPbis wgで、サーバが明示的にCTに対応している旨をブラウザに通知するExpect-CTヘッダの議論が行われる予定です。これにより、何かの不具合が生じたときに検知できるようになります。

Expect-CT ヘッダ

まだ、議論が開始するような段階のためまだまだ決定したものではありませんが、Expect-CT ヘッダは以下のようなもののようです。(今のところ著者の個人githubリポジトリより仕様が確認できます。)



CTに対応してるWebサーバは、httpsで接続を受けた際、レスポンスヘッダにExpect-CTヘッダを付加します。

expect-ct: enforce;max-age=3600;report-uri=https://example.com/report-uri


これを受け取ったユーザエージェントは、それ以降の接続でCT対応のサーバ証明書が得られなかった場合は何かがおかしいと判断します。その際、Expect-CTヘッダで指定されたreport-uriにレポートを送信します。


Expect-CTヘッダには、以下のようなディレクティブが指定できます

  • enforce: CTポリシーに違反した際、接続を拒否するように指示する
  • max-age: このExpect-CTヘッダの有効期間を秒で指定
  • report-uri: CTポリシーに違反したときのレポート送信先絶対URL


レポートされる内容は、jsonで以下の通りです。

{
  "date-time": date-time,
  "hostname": hostname,
  "port": port,
  "effective-expiration-date": expiration-date,
  "served-certificate-chain": [ (MUST be in the order served)
    pem1, ... pemN
  ],
  "validated-certificate-chain":
    pem1, ... pemN
  ],
  "scts": [
    sct1, ... sctN
  ]
}

時刻と、サーバの情報、及び証明書チェーンとsctが送られるようです。

CloudFlareの提案するHTTP/2の圧縮辞書拡張

IETFのHTTPbis wgでCloudFlareの方より「Compression Dictionaries for HTTP/2」(URL)という仕様が提案されている。


これは、Content-Encodingヘッダで指定される圧縮アルゴリズム向けの初期ウィンドウに使用される辞書データを事前に送る拡張を定義する。


CloudFlareではすでに検証されており、通常のDeflate(zlib compression level 8))より最大1.50倍、平均で1.10倍縮小できたとしている。

背景

  • CSS/JS/HTMLといったアセットは、Content-Encodingを使用しDeflateやBrotliで圧縮される
  • HTTP/2以前は、リクエスト数を少なくするためにインライン化などを行っていた
  • 圧縮アルゴリズムは、後方一致するほど圧縮率が上がる
  • HTTP/2で小さいファイルを多重化して送ることが推奨化されており、圧縮率が上がらない。

拡張フレーム

この仕様では、2つの拡張フレームを定義している。大雑把に言うと辞書を宣言するフレームと、辞書を適応するフレームである。

SET_DICTIONARYフレーム
   +-------------+-------------+
   | Dict ID (8) |   Size (8)  |
   +-------------+-------------+
  • Dict ID: 辞書のためのスロットを指定する値
  • Size: 辞書のサイズ
USE_DICTIONARY
   +-------------+
   | Dict ID (8) |
   +-------------+

Dict ID: SET_DICTIONARYでセットされた辞書のスロットを指定する

動作

サーバ側

サーバは、最初のDATAフレームを送る前にいかなるストリーム上でSET_DICTIONARYフレームを送信できます。サーバは最初の2^Sizeの圧縮されてないオクテットを後続のストリームのためのCompression Dictionaryとして使用します。


サーバはストリームでDATAフレームを送信する前にUSE_DICTIONARYフレームを送ることで、Compression Dictionaryでストリームを圧縮できます。

クライアント側

クライアントはSET_DICTIONARYフレームを受信するとそのサイズ分だけ辞書のためにスロットを予約します。
そのストリームでDATAを受信したあと、最初の2^Sizeオクテットを辞書として保存します。


USE_DICTIONARYを受け取った際、クライアントはDATAの複合に指定された辞書を使用します。

googleの新しい時刻同期プロトコル Roughtimeとは

[追記] 2020年1月時点の動向について、新しく記事を書きました
asnokaze.hatenablog.com


Googleの「Adam Langley氏のブログ」で、新しい時刻同期プロトコルについて紹介されている。このRoughtimeは特定のタイムサーバに依存しないセキュアな方法で時刻同期を行うことを目的としたプロトコルです。すでに、googleのサーバで動作しており、roughtime.sandbox.google.com:2002に向けて公開されている専用クライアントで接続できる。


現在のセキュリティは現在の時刻に依存しており、その重要性はましています。証明書の有効期限や、OCSPレスポンス、Kerberosのチケット、DNSSECの応用やPGP鍵といった機能でも重要です。しかし、Chromeの証明書エラーのうち25%はローカルの時刻に起因するとしているとしています。


現在、最も使用されているNTPプロトコルの認証機能はほとんど使用されていないとし、MITM攻撃で時刻を操作することが出きる旨記述している。


この問題に対して、それらを改善することを目的に新しい時刻同期プロトコルroughtimeを提案している。

roughtimeの特徴

  • UDPプロトコル
    • 増幅攻撃を防ぐため、リクエストメッセージは1kバイトになるようにパディングされる
  • 現段階では正確な時刻同期を目標としておらず、10秒以下であれば満足とする
  • 認証されているタイムサーバでも複数のタイムサーバを用いて正しくない振る舞いをするタイムサーバを検出する
  • 公開鍵方式で署名される
    • Ed25519のみをサポートしてる
    • サーバのレスポンスは、クライアントが生成して提出したノンスと時刻について署名して応答される
    • long-term identity key から委譲されるonline public keyを使用する
    • 複数のリクエストに対してバッチ的に署名する (Merkle treeのルートに対して署名する)
    • レスポンスに含まれるMerkle treeは、実際には対象のノンスから、ルーに到達するのに必要なノードとindex(left or rigth)が与えられる(木のリーフは1~64)
    • Skylakeチップで1コアで1秒あたり430万リクエストを署名できると見積もっている
  • うるう秒を24時間に分割して時刻に反映する

roughtimeプロトコル概要

クライアントは乱数を生成し、それをnonceとしてRoughtime リクエストをサーバに送信します(1024バイトまでパディングされる)。サーバはそのノンスと時刻に対する署名、サーバ証明書などをroughtimeレスポンスとして返します。


この時、クライアントが生成したノンスに対して署名されて返ってくるため、その時生成されたことが保証されます。


つまり、サーバAに問い合わせて結果を得たあとに,サーバBに問い合わせた場合。サーバAから得られた時刻 より、サーバBから得られた時刻のほうが新しいはずということがわかります。この時サーバBに問い合わせるときに、サーバAからの応答をハッシュしてノンスとして使えば、その順序も確実であるといえます。それぞれから得られた時刻で矛盾があればなにかおかしいことに気づくことができます。


この手続を6つ以上の独立したサーバで行えば不正なサーバを検出でき、正しい時刻を定めるのに十分正確だとしています。

roughtimeメッセージ

Roughtime メッセージ(リクエスト or レスポンス)は、タグの数を示すヘッダと、複数のタグと値のペアからなります。タグ名は32bit値であり、ドキュメント中では文字として"NONC "や"CERT"と記述されます。3文字の場合は4文字目だけ16進数で明示的に記述されます("SIG\x00")。タグはそれぞれ値をもちます。

roughtime リクエス

Roughtime リクエストは、NONCタグを持つメッセージです。NONC の値は64バイト値です。それ以外のタグは無視されます。増幅攻撃を防ぐためにPAD\xffタグを用いて1024バイト以上にする必要があります。

Roughtimeレスポンス

レスポンスは以下のタグで構成されます

  • SREP: 署名されるメッセージ。Merkle treeの根と、時刻を示す2つのタグMIDP,RADIを含みます
  • SIG\x00: SREPの署名値である64バイト値。Ed25519 のみが署名アルゴリズムとしてサポートされており。
  • CERT:サーバの証明書。署名に使用するonline public keyと有効期間と、この鍵に対する署名。
  • INDXPATH: 複数のクライアントのリクエストに含まれるノンスはMerkle treeの葉となる。複数のリクエストに対して署名を行うため、Merkle treeのルートのみに署名を行う。このMerkle treeをルートまでたどるのに必要な経路PATH(ノードのリスト)と、たどる際のノードが左右のどちらのノードなのかを示すINDX
Roughtime UTC

タイムスタンプはMIDPRADIの2つのタグで表現されます。MIDPタグはマイクロ秒で時刻範囲の中心点をunit64として持ちます。一方RADI タグは時刻範囲の半径をマイクロ秒でuint32で表現します。


Roughtime の"正しい時刻"は、24時間でうるう秒をならしたUTCです。1日かけて均等に反映されていきます。

実装

C++ と Goの実装が公開されている。 C++はbazelを導入し、ビルドする必要があるが、Goは比較的すぐに動作確認することができる。

vagrant@vagrant:~/go/gopath/src$ git clone https://roughtime.googlesource.com/roughtime roughtime.googlesource.com

vagrant@vagrant:~/go/gopath/src/roughtime.googlesource.com/go/client$ go build

vagrant@vagrant:~/go/gopath/src/roughtime.googlesource.com/go/client$ ./client --servers-file=../../roughtime-servers.json --chain-file=$HOME/roughtime-chain.json
Quorum set to 1 servers because not enough valid servers were found to meet the default (3)!
real-time delta: -16.306032ms


roughtime-servers.jsonGoogleのサーバとその公開鍵が指定されており時刻の誤差が表示される、一つしかないのでサーバの検証は実施されない。


20/10/11 表現を少々修正しました
前) 正確な時刻同期を目標としておらず、10秒程度の誤差は許容する
後) 現段階では正確な時刻同期を目標としておらず、10秒以下であれば満足とする