要約

  • ALPN ではクライアントが不透明な識別子を順序付きで提示し、サーバーが共通集合から一つを選ぶ。その値が拘束するのは当該 TLS 接続のアプリケーションデータであり、別の接続やサービス全体ではない。
  • 終端プロキシは下流ではサーバー、上流では新しいクライアントになる。下流が h2、上流が http/1.1 でも矛盾しない。証拠は二本の接続と変換イベントを分けて残す必要がある。
  • IANA 登録、ソースコード、設定、コールバック設置は実行候補にすぎない。提示、選択、失敗、実際のコールバック呼び出し、握手完了、アプリケーション開始を接続単位で確認して初めて運用上の事実になる。

終端点で切れたはずの権限

下流のパケットだけを読むと結論は明快だった。ClientHello に二つの候補があり、TLS 1.3 の EncryptedExtensions で h2 が返り、Finished がトランスクリプトを認証した。さらに HTTP/2 の connection preface と SETTINGS が続いた。

ところがエッジは復号したリクエストを別の接続へ載せ替えた。上流側ではエッジ自身がクライアントになり、別の鍵、別の証明書検証、別の ALPN 方針を使う。そこで http/1.1 が選ばれたなら、オリジンについて言えるのは上流の結果だけである。

事故を起こしたのはプロトコル変換ではない。変換は意図した設計になり得る。問題は監視基盤が下流接続 ID を捨て、選択値をオリジンの恒久属性へ書き込んだことだ。ALPN の強さを保つには、主張を「この二端点間のこの接続」に戻さなければならない。

文字列に見えても比較対象はオクテット列

ALPN の拡張値は 16 である。RFC 7301 の ProtocolName は、長さ一バイトを前置した 1~255 バイトの不透明値だ。空要素と途中で切れた要素は無効である。

表示層が小文字化したり、空白を除いたり、似た名前へ寄せたりしてはいけない。TLS 上の HTTP/2 は二オクテットの h2 である。RFC 9113 は TLS で h2c を送ることも選ぶことも禁じている。見た目が近い識別子は同じ意味を持たない。

クライアントの並びは希望順を表すが、決定権を独占しない。サーバーは相互に扱える値の中から自らの優先順位で選べる。したがって、先頭提示と実際の選択を別欄に残す必要がある。

共通値がなければ、RFC 7301 は致命的な no_application_protocol を要求する。RFC 9325 も、提示されていないプロトコルを受け入れることによるクロスプロトコル攻撃を避けるため、厳格な処理を勧告している。

選択後に必要な、もう一つの確認

選ばれた値は同じ接続上のアプリケーションデータに対して決定的である。h2 を返したサーバーが、その接続を HTTP/1.1 として扱ってはならない。

HTTP/2 は TLS の後に双方が connection preface を送る。これは重要な第二の観測点だ。ClientHello は提示、EncryptedExtensions は選択、Finished は認証済み握手、preface と SETTINGS はプロトコル開始をそれぞれ示す。

どれか一つで上位の状態を代用してはならない。途中までのキャプチャは完了を証明せず、ALPN 成功だけでは HTTP/2 パーサーの起動を証明しない。一回の成功は次の接続にも引き継がれない。RFC 9113 自身、インスタンスや構成、ネットワーク条件が変わるため過去の対応実績は将来接続の強い根拠ではないと述べている。

プロキシには二つの台帳が要る

終端プロキシは下流 TLS のサーバーであり、上流 TLS のクライアントである。接続ごとに提示値、選択値、アラート、証明書、ポリシー世代が異なる。

Envoy の上流プロトコル設定は、この独立性を具体的に示す。下流のプロトコルを明示的に使う方式もあれば、上流で ALPN を行って HTTP/1.1 と HTTP/2 を独立選択する方式もある。上流 ALPN がなければ HTTP/1.1 へ戻す動作も構成できる。

「下流に合わせる」さえ明示的な方針である以上、暗黙の連続性を仮定してはならない。記録すべきなのは、下流の接続タプル、上流の接続タプル、そして終端・変換・トンネルのどれを行ったかである。

CONNECT のような真のトンネルでは、中継者が内側の TLS 終端ではないこともある。RFC 7639 の ALPN HTTP ヘッダーはトンネル内で使う予定のプロトコルを伝えられるが、予定は完成した内側握手の証拠ではない。

OpenSSL が示す実行時の分岐

OpenSSL ではクライアント側リストを長さゼロにすると ALPN 拡張自体を送らない。ClientHello に提示がなければ、サーバーの ALPN 選択コールバックは呼ばれない。コールバックが登録済みでも、実行証拠にはならない。

さらに servername コールバックが先に動き、SSL_CTX を入れ替え得る。その後の ALPN コールバックは新しい文脈を使う。静的な全体設定より、どの SNI 方針が接続に適用されたかを残すべき理由である。

選択値はクライアント入力の一項目でなければならない。SSL_get0_alpn_selected は NUL 終端されない借用メモリーを長さ付きで返す。長さを失って普通の文字列として扱うログは、値を切断したり余分なメモリーを読んだりする。

SSL_select_next_proto には試験すべき落とし穴がある。共通値がなくてもクライアント先頭値を出力へ置き、同時に OPENSSL_NPN_NO_OVERLAP を返す。ALPN ではその出力を無視しなければならない。戻り値を確認せず選択値だけ記録すると、存在しない合意が作られる。

また致命的な不一致と NOACK は別結果である。後者はこの接続で選択しない状態を表せる。未提示、構文不正、共通値なし、選択なし、コールバック失敗、選択成功を一つの真偽値へ潰してはならない。

再開接続と 0-RTT の狭い継承

RFC 7301 は ALPN をセッションではなく接続の性質と定義する。再開やセッションチケットを使っても、以前の拡張内容は無関係で、新しい握手が新しい接続を決める。

これは更新中の運用で重要だ。過去に h2 を選んだチケットが、別インスタンスや新設定にも h2 を強制するわけではない。監視は古い属性を複写せず、新しい接続の結果を取る。

TLS 1.3 の 0-RTT では、PSK に ALPN を含むパラメータが結び付く。サーバーが早期データを受け入れられるのは、新しい選択がその ALPN と一致するときだけだ。拒否後に別プロトコルが決まれば、アプリケーションはメッセージを作り直す必要がある。

そのため RFC 9846 は、同じ ALPN が選ばれない限り TLS 実装による早期データの自動再送を禁じる。フレーミングと意味を理解するアプリケーションだけが再構成の可否を判断できる。

プロトコル、本人性、認可を三分する

ALPN が選ぶのは会話方式であり、相手が意図したサービスかどうかではない。RFC 9525 のサービス本人性照合や証明書パス検証は別に実行される。

HTTP/3 はこの違いを分かりやすく示す。QUIC 上の TLS で通常 h3 を選ぶ一方、RFC 9114 は URI のオリジンサーバーに対する証明書検証も要求する。h3 が正しくても証明書が合わなければ、そのサーバーは対象オリジンの権威ではない。

本人性が確認できても、テナント、経路、メソッド、資源への認可はさらに別である。IANA 登録も同様に、識別子の意味を定義するだけで稼働を証明しない。運用記録は ALPN、本人性、認可を三つの結果として保存すべきだ。

境界を守る負の試験

クライアントに h2,http/1.1 を提示させ、サーバーは http/1.1 を優先する。監視が先頭値ではなく選択値を保存するか確認する。提示外の値を返した場合は握手が停止しなければならない。

ALPN なしの ClientHello ではコールバックが呼ばれないことを確認する。共通値なしの致命アラートと、意図した NOACK を分ける。SSL_select_next_proto の不一致出力が選択として残らないこと、SNI による SSL_CTX 交換が正しい世代へ帰属することも試す。

プロキシでは下流 h2・上流 HTTP/1.1 を作り、二本の接続 ID と変換を保存する。再開時には以前の ALPN を捨てる。0-RTT と最終 ALPN を変えて自動再送が起きないことを確かめる。

最後に、ALPN 成功後の証明書本人性失敗、本人性成功後のアプリケーション拒否を発生させる。三つの結果が別々に見えるなら、プロトコル台帳はようやく権限境界を守っている。

出典