HTTPS のはずなのに警告が出てめちゃ調べたけどよくわからなかった

このサイトは Cloudflare で Astro の静的コンテンツを配信する形で公開しています.移行が推奨されていたので,先日 Pages から Workers (Static Assets) にホスティングを変更しました.おそらくその前後から携帯電話 (iPhone) で自分のサイトを開くと,ときどき「この接続は安全ではありません」とかいう警告が出るようになりました.

色々と観測した範囲では正常に接続できているようで,警告された不具合は無いようでした.とりあえず,対症療法で警告が出ない状態にはできましたが,どうして偽陽性の判定が出てしまうのか,その根本的な理由まではわかりませんでした.

この記事は,どんな感じで調べていたかの記録です (詳しいひと助けて).

起きていたこと

だいたい,こんな感じでした.

  • このサイトの URL (silasol.la) を踏んだとき,たまに警告が出る
    • よくある「この接続は安全ではありません」「この Web サイトは HTTPS での安全な接続に対応していません。クレジットカード、電話番号、パスワードを含む、この Web サイトで閲覧したり入力したりする情報は、ほかの人に読み取られたり変更されたりする可能性があります。」って汎用メッセージが全画面で
    • 観測した範囲では Twitter や Bluesky のネイティブアプリから,スマートフォン (iPhone) で開いたときに出る
  • 毎回出るわけではなく,何度かリンクを押していると出たり出なかったり
    • リロードするだけで警告は消える
    • うまくいった次の回にまた出ることもある
    • PC のブラウザでは一度も出たことが無い
    • Wi-Fi でも携帯回線でも出ている
  • 証明書は有効で,期限も発行元も問題ない

ちょっと,調べるのがめんどくさいやつです.

HTTP まわりの確認

安全でない接続だと言われていますが,リロードするだけでページ自体は普通に開くし,よくある証明書まわりではなさそうです.

とりあえず,リンクが http:// からのリダイレクトになっていないかを見てみました.Twitter だと見た目は普通のリンクでも強制的に短縮 URL (t.co) から転送していたりするので,そのあたりを確認します.

curl -sI "https://t.co/..." | grep -i location
# => location: https://silasol.la/

普通に https:// へ転送していて問題なさそうです.

ここで設定をこねていたら少し進展があって,(モバイルで安定していないのかなってことで) HTTP/3 1 を Cloudflare 側で切ったら警告が出なくなりました (逆に ON にしたら警告が復活する).QUIC まわりかなってことで,このへんをよく調べることにしました.

サイト自体が HTTP/3 で失敗していないか確認するために,手元にある Mac の curl から --http3-only で HTTP/3 を強制 2 してリクエストを送ります.

curl --http3-only -sI https://silasol.la/
# => HTTP/3 200

応答は正常で,サイト側は特に問題なさそうです.

ネットワークの問題でもなさそう

ネットワーク経路の問題なのかなとも思いましたが,携帯をセルラー回線に切り替えても出るし,同じ Wi-Fi に繋いでいる Mac からは何度 curl でリクエストを送っても成功しています.

当初は Twitter のアプリ内ブラウザでばかり出ていたので,クライアントの実装や WebKit 固有の問題なのかなとも思っていました.しかしながら,どうやら Bluesky のリンク (こちらは直リンク) から遷移した Safari でも出るようで,Twitter に限った問題ではなさそうです.手元にある iPhone でしか試していないので,これが広く起きる話なのか,端末や環境に固有なのかまでは分かりませんが.

ここまで,外部から確認できる範囲では,どこも正常に動いているようでした.

パケットを見てみる

通信の中身についても見ておこうと思って,パケットをキャプチャしてみました.アプリ内は DevTools とかで見られないので,端末の通信自体を記録することにします.iPhone を Mac に繋ぐと RVI 3 を作ることができて,そこに流れる通信を丸ごとキャプチャします.

# rvi0 という仮想インターフェースができる
rvictl -s '<iPhone の UDID>'
# ファイルに書き込み
sudo tcpdump -i rvi0 -n 'port 443' -w trace.pcap

これを Wireshark (tshark コマンド) で読んでみました.まずは QUIC 接続先 (ClientHello の SNI 4) の確認からです.

tshark -r trace.pcap -Y 'quic && tls.handshake.type == 1' -T fields -e tls.handshake.extensions_server_name
silasol.la
silasol.la
silasol.la
silasol.la

それぞれの行が QUIC 接続 1 つの ClientHello です.接続先はすべて silasol.la でした (ブラウザが接続を複数張っていて複数行ある).いずれも平文で見えており,ECH で接続先 SNI を暗号化しているわけでもなさそうです.

サーバ (エッジ) からレスポンスが返っているかについても確認します.ipv6.src をエッジの IP アドレスに絞ると,サーバから端末へのパケットを出せます.

tshark -r trace.pcap -Y 'quic && ipv6.src == <エッジ IP アドレス>' -T fields -e frame.time_relative -e _ws.col.Info
2.134493   Initial, SCID=0124…, PKN: 0, ACK
2.134494   Initial, SCID=0124…, PKN: 1, ACK, CRYPTO
2.136298   Protected Payload (KP0)
2.136299   Protected Payload (KP0)
2.148406   Initial, SCID=01da…, PKN: 1, ACK, CRYPTO
2.148407   Protected Payload (KP0)
2.174419   Handshake, SCID=0124…

Initial … ACK, CRYPTO からサーバがクライアントに応答している 5 こと,Handshake からハンドシェイクが進行していることが分かります.Protected Payload (KP0) は,ハンドシェイクが完了して実際に流れている暗号化されたデータ (ページの内容) です.もしも接続が失敗していれば,こういった行は出てこないはずです.

そういうわけで,警告が表示されている最中でもサーバは応答しており,ちゃんと暗号化データも届いていました.エラーも切断も発生しておらず,接続自体は成功しているようです.

このまま Cloudflare の設定で HTTP/3 を切ってみると,クライアントは以前の alt-svc 6 を頼りに QUIC を試し,サーバ側で拒否されて TCP にフォールバックしました.その TCP も成立してページも表示されますが,その瞬間は警告が出てしまいます (アプリを再起動すると出なくなる).接続の成否自体が原因ではないようでした.

警告が出た回と出なかった回のキャプチャも比較しましたが,QUIC の成否も,ハンドシェイクの再送も,接触したホストも,エラーの有無も,どうやら差は無いようでした.観測する限り,警告が出ているのは間違いで,実際の通信に問題は起きていないようです.

他のサイトとの比較

同じ構成 (Cloudflare + HTTP/3) で個人サイトを Twitter で公開している人がいたので,同じように iPhone のアプリから開いてみましたが,自分のサイトのような警告は出ませんでした.私のサイトと比べてみると,どうやら 0-RTT (Early Data) 7 の設定が異なるようです (その人のサイトは無効で,私のサイトは有効).これは実際に TLS 1.3 の Early Data を送信して確認できます.

# セッションの取得
printf 'GET / HTTP/1.1\r\nHost: silasol.la\r\nConnection: close\r\n\r\n' \
  | openssl s_client -connect silasol.la:443 -servername silasol.la \
      -tls1_3 -sess_out sess.pem -ign_eof >/dev/null 2>&1
# Early Data を送信できるか試す
printf 'GET / HTTP/1.1\r\nHost: silasol.la\r\nConnection: close\r\n\r\n' > req.txt
openssl s_client -connect silasol.la:443 -servername silasol.la \
  -sess_in sess.pem -early_data req.txt </dev/null 2>&1 | grep -i 'early data'
# => Early data was accepted

ここでの確認は TCP の TLS 1.3 Early Data ですが,パケットキャプチャでは QUIC の 0-RTT パケットの送信も確認しています.

そういうわけで,HTTP/3 は有効のまま 0-RTT だけを Cloudflare で無効にしてみたら,警告は出なくなりました.めでたしめでたし.

いや普通に後味は悪くて,0-RTT をやめると出なくなるのは良いのだが,どうして 0-RTT で,しかも毎回ではなく間欠的に誤判定されるのか,理由は謎です 8.最終的な判定はクライアントで行われるので,外からは見えずじまいといったところでしょうか.

最終的な対処

今後は 0-RTT を OFF にしておきます.静的サイトだしそれほど惜しくはありません.HTTP/3 を維持できてよかった.

0-RTT の Early Data には,通常の通信より弱い面もあるようです.RFC 9846 には,Early Data が前方秘匿性を持たず,接続を跨いだ再送に関して保証しないと明記されていました 9.今回のケースと関係するかは分かりませんが,まあ OFF にしても実害は無いでしょう.

同じような現象に心当たりのある方がいれば,根本的な原因や対処方法など教えていただけると喜びます.m(_ _)m

脚注

  1. TCP ではなく QUIC (UDP を利用) で通信する HTTP です.同じ 443 ポートでも土台の通信が異なります.

  2. HTTP3 が利用できるようにビルドされた curl が必要です.curl -VFeatures:HTTP3 があるかで判別できます.macOS に同梱されたものは対応しておらず Homebrew で入れました.

  3. 端末の通信を Mac の仮想インターフェース (Remote Virtual Interface) に写せます.コマンド類は Xcode で入ります.

  4. TLS ハンドシェイクにおける最初のメッセージを ClientHello と言います.このパケットの中で,クライアントは SNI (Server Name Indication) という拡張機能を用いて,これからアクセスしようとしているドメインを伝えます.

  5. ACK は確認応答で CRYPTO は TLS ハンドシェイクの中身です.

  6. サーバがクライアントに対して,別のプロトコル (HTTP/3 など) やホストで接続できることを通知する仕組みです.ブラウザや端末で (最大 24 時間) キャッシュします.

  7. 再接続したときに,ハンドシェイクを完了する前からでもデータを送り始めることができる TLS 1.3 の仕組みです.

  8. NAMIKIBASHI 日本の形「宴」

  9. 2.3 節の IMPORTANT NOTE にあります.