TLS とセキュリティ 基礎

curl の TCP ハンドシェイク直後に走る 2 つ目のハンドシェイクについて、3 つを示す。暗号化だけでは何も買えないこと、 key share 1 つが 1 往復と 2 往復の差のすべてであること、TLS の高い部分は暗号化ではないこと。

01

保護されていないバイトはこう見える

curl とサーバの間にはワイヤを読める機械が 6 台ある。TLS は接続を隠さない。読まれる中身を無意味にするだけだ。

各ホップはリンクを終端してパケットを転送し、その間そのバイト列を自分のメモリに置く。攻撃ではなくルーティングそのものだ。問題は届いたバイトの中身だけである。

観測者を経路上でドラッグし、スキームを切り替えてみる。素の http:// ではどのホップもリクエスト全体を読む。https:// では同じホップが読めるのはヘッダとノイズだけだ:

カフェの Wi-Fi —— リクエスト全体。観測者を経路上でドラッグ;矢印キーは 1 ホップずつ、Home で戻る
カフェの Wi-Fi —— リクエスト全体

観測者がワイヤから離れていないことに注目してほしい。TLS はパケットを誰が見られるかを一切変えない ——カフェのアクセスポイントは今も全バイトを転送している。変わったのは、URL も cookie もボディもそもそもパケットの中に存在しないという点だ。

機密性は 3 つの保証のうち最初の 1 つでしかなく、単独ではほぼ無価値である。暗号文のバイトを 1 つ反転させ、アプリケーションに何が届くか見てみよう —— まず暗号化だけの方式、次にAEAD タグ付きで:

バイト 7 を反転 —— 平文のビットが反転、レコードは受理

バイト 7 を見てほしい。タグのないストリーム暗号では、暗号文の 1 ビットを反転すると平文の同じビットが反転する。amount=10.00 が amount=90.00 になり、受信側には見分けがつかない。攻撃者は鍵を知らないし、必要もない。AEAD なら同じ改変は bad_record_mac になり、レコードは捨てられる。

残るのは真正性で、人が真っ先に落とすのがこれだ。証明書検証を切った状態で傍受者を 1 ステップずつ進め、次に検証を入れてもう一度:

ステップ 1 / 4

クライアントが証明書の署名者を尋ねないので、攻撃者は完全に暗号化されたハンドシェイクを —— 自分自身と —— 成立させ、本物のサーバへ中継する。検証を入れるとステップ 3 で unknown_ca (48) により止まる。誤った相手への暗号化は弱い保証ではなく、保証が無いということだ。

TLS はこの 3 つを 1 つの層で買う。アプリケーションの下、TCP の上に座る層だ。スタックを下りながら、同じ 176 バイトのリクエストが各層で何バイトになるか見てみよう:

アプリケーション

レコード層は 22 バイトを足す。5 バイトのヘッダ、暗号文の内側に隠された 1 バイトのコンテンツタイプ、16 バイトのタグ。中身が何かは知らない。HTTP も IMAP も gRPC も AMQP も同じ扱いで、だから stunnel は TLS を知らないプロトコルを包める。

実務上の帰結が 1 つあり、それが障害の出どころを決める。TLS は秘密鍵のある場所で終端する。本番ではそこはまずリクエストを処理するプロセスではない —— ロードバランサがバックエンドと話す 3 通りをスライダで動かしてみよう:

VPC 内の平文 HTTP

真ん中の箱が証明書を持つので、バージョンポリシーも暗号スイート一覧も有効期限もそこに住み、TLS 障害のほとんどもそこで起きる。その後ろでは平文 HTTP が平文の区間を 1 本残し、誰も経路を誤らず隣のホストでシェルも取られないことに賭けている。

02

2 つの暗号ファミリと、それぞれが安い場所

片方は速いが双方が既に鍵を共有している必要があり、もう片方は遅いがその必要がない。両者の比がプロトコルの形を決める。

対称暗号は同じ鍵で往きも復りも回す。AES は Westmere 以来ライブラリではなく命令で、サーバのコアでは計算よりメモリ帯域の問題に近い。公開鍵演算はそうではない。

ペイロード長をドラッグし、3 つを同じ対数軸で読む ——バルク暗号化とX25519 交換 1 回、P-256 署名 1 回:

ペイロード 1 KB

初期値の 1 KB ではバルクの仕事が 0.20 µs、署名が 29 µs —— 147 倍だ、バイト数は 400 分の 1 なのに。上へドラッグすると 2 つのマーカーが並ぶのは 128 KB と 256 KB の間である。設計の全部がこれだ。公開鍵のコストは接続ごとに 1 度払い、あとは二度と払わない。

非対称が遅いのは、鍵 1 ビットが買える安全性が少ないからだ。安全性レベルを進めて、RSA と曲線がそれぞれ何ビット要るか比べてみよう:

80 ビットの安全性

上るほど差が開くことに注目してほしい。112 ビットではRSA が 2,048 ビット、曲線が 224 ビット —— 9 倍。256 ビットでは 15,360 対 512 —— 30 倍だ。新しいサーバ証明書がどれも ECDSA P-256 なのはこのためで、RSA-3072 と同じ 128 ビット強度を 12 分の 1 の鍵材料で出す。(NIST SP 800-57 Part 1 Rev. 5、表 2。)

改ざんを検出できるようになるまでどちらのファミリも使い物にならず、現代の TLS はその検出を暗号そのものに畳み込んでいる。レコードは 16 バイトのペイロードから始まる —— 上へ動かし、タグの値段が消えるのを見よう:

ペイロード 16 B

ペイロード 16 バイトのとき、レコードはワイヤ上で 38 バイト —— オーバーヘッド 58% だ。16 KB の上限では 0.13% になる。AEAD は暗号化と認証を 1 回の呼び出しにするので、間違える順序も、復号と検査の間の分岐も存在しない。次の図はその一文の値段である。

AEAD 以前これは 2 回の呼び出しで、順序が決定的に効いた。受信側に両方の順序を歩かせ、MAC を確認する前に攻撃者が選んだバイトに触るのはどちらか見てみよう:

ステップ 1 / 3

MAC してから暗号化は先に復号してパディングを剥がすので、偽造レコードを拒否するまでの時間が攻撃者の選んだパディングに依存する。その時間差が復号オラクルであり、BEAST と POODLE と LUCKY13 の正体だ。暗号化してから MAC は復号が走る前に偽造を捨てる。AEAD にはそもそも間違える順序が無い。

AEAD が唯一耐えられないのが nonce の再利用である。2 本目以降の任意のレコードへ移り、nonce をシーケンス番号から固定値に切り替えてみよう:

レコード 1 —— 他で使われない鍵ストリーム

1 つの鍵の下で 2 本のレコードが nonce を共有した瞬間、両者は鍵ストリームを共有し、 2 つの暗号文を XOR すればそれが打ち消される —— 攻撃者は鍵に触れずに P₀ ⊕ Pᵢ を得る。GCM では認証用の副鍵まで落ち、偽造がタダになる。TLS 1.3 は重複しえないシーケンス番号から nonce を導き、この選択肢を消した。

03

公開の場で鍵に合意する

両端は同じ対称鍵を必要とし、互いに言うことは経路全体に読まれる。これを 1 往復で解くのが、 TLS の残り全部が乗る手品だ。

素直な答え —— ランダムな鍵をサーバの公開鍵で暗号化して送る —— が TLS 1.2 のやり方で、TLS 1.3 が削除したものだ。消えた理由は遅いからではない。

置き換えた側から始めよう。クライアントの秘密値とサーバの秘密値を別々に設定する。ワイヤを渡るのは 2 つの公開値だけで、それでも両者は同じ数に着地する:

クライアント秘密値 a = 6 · サーバ秘密値 b = 15

共有値が両端の箱にだけ現れ、真ん中には決して現れないことに注目してほしい。2 つの公開値を持つ観測者がそこへ行くにはa か b を復元するしかない —— 手持ちを掛け合わせても出るのは 5a+b であって 5ab ではない。実際の TLS は mod 23 ではなく Curve25519 上で走るが、形はこのままだ。

安全性のすべてはその復元のコストであり、今も使われている群の間で 5 桁も違う。順に進めて、log₂ 軸から作業量を読み取ろう:

DH-1024

DH-1024 は 280 にあり、数体ふるい法の作業の大半は素数ごとに 1 回で済む。だから広く共有された 1024 ビット素数 1 つに対して事前計算した相手は、それを使った接続をすべて読める。これが Logjam(Adrian ら、CCS 2015)であり、TLS 1.3 が有限体 Diffie-Hellman を落とした理由だ。X25519 には事前計算の対象になる共有パラメータが存在しない。

後日の窃取を生き延びる性質が前方秘匿性で、鍵転送が消えねばならなかった理由そのものである。セッション鍵の合意方法を選び、サーバ秘密鍵が漏れる日をドラッグしてみよう:

0 日目に鍵が盗まれる

マーカーの後ろのバーを見てほしい。RSA 鍵転送では 0 日目まで遡って記録されたセッション全部が復号できる。セッション鍵が、それより長生きする鍵で暗号化されていたからだ。ECDHE では1 つも復号できない。一時的な秘密値はどこにも書かれておらず、盗まれた鍵は署名しかしていない。

32 バイトの共有鍵 1 つでは足りない —— 接続には方向ごと・フェーズごとに別の鍵が要る。さもないと片方向のメッセージがもう片方向へ再生できてしまう。鍵スケジュールを下ってみよう:

ECDH 共有鍵

矢印 1 本が label 文字列の異なる HKDF-Expand-Label 呼び出し 1 回で、出力どうしは計算上独立である。だからアプリケーション鍵が破られても再開 secret は渡らないし、KeyUpdate がハンドシェイクなしで鍵を回せる。

この軸への最後の圧力は、Shor のアルゴリズムを走らせられる機械だ。対策はすでに配備済みで、支払いはバイトで行われる —— 群を順に進め、ClientHello がセグメント上限を越えるのを見てみよう:

X25519

X25519MLKEM768 は 1,216 バイトの key share を運び、ClientHello を 1,528 バイトへ押し上げる —— イーサネット MTU が残す 1,460 を超えるので hello は 2 パケットになり、2 つ目が届くまで完成しない。Chrome は 131(2024 年 11 月)でこれを既定にした。追加 1 パケットが代金の全部だ。

04

暗号化レコードまで 1 ラウンドトリップ

TLS 1.2 は最初の HTTP バイトまでに 2 往復を要した。TLS 1.3 は 1 往復。違いは最初のメッセージでの推測 1 つだ。

推測するのは key share だ。サーバも対応していそうな群を選び、一時鍵ペアを生成して公開側をただちに送る —— 外れたら 1 往復増えるのを承知の上で。

ハンドシェイクをステップ実行または再生し、各メッセージがどの鍵で守られているか見てみよう。平文、次にハンドシェイク鍵、そしてアプリケーション鍵:

key share 付きの ClientHello

暗号化が始まるのがどれだけ早いかに注目してほしい。平文なのは 2 つの hello だけで、 4 通目にはサーバはもう暗号化している。TLS 1.3 で証明書がワイヤ上に現れないのはこのためだ。クライアントの Finished がハンドシェイクを閉じ、リクエストは次のパケットに乗る。

節約された 1 往復がこの再設計で買えた唯一のもので、その価値は完全にネットワーク次第である。ラウンドトリップをドラッグし、応答の最初のバイトまでの時間を読もう:

ラウンドトリップ 80 ミリ秒。横にドラッグしてラウンドトリップを変える;矢印キーは 5 ミリ秒ずつ、Home で戻る
ラウンドトリップ 80 ミリ秒

初期値の 80 ミリ秒では、TLS 1.3 が 240 ミリ秒で最初のバイトに届き、TLS 1.2 は 320 ミリ秒かかる —— 有用なことがまだ何も起きていない待ち時間から 80 ミリ秒が消える。ここで数えているのは、TCP に 1 往復、TLS に 1〜2 往復、リクエストとその答えに 1 往復。300 ミリ秒の衛星回線なら節約は 300 ミリ秒になる。

再開はさらに進んでリクエストを最初のパケットに入れられる。ただし設定では消せず、設計で回避するしかない落とし穴が付いてくる。early data が運ぶメソッドを選び、捕まえたパケットを再送してみよう:

0 回再送 —— 毎回同じ答え

サーバには照合できる接続ごとの状態が無いので、再送と本物を区別できない。4 回再送までドラッグすると、この口座は 5 回請求されている。TLS 1.3 はこれを直していない —— 本物の再送防止は CDN の全エッジノードで共有する状態を要求するからだ。だから 0-RTT は GET にだけ許され、状態を変えるものにはフルハンドシェイクが強制される。

ハンドシェイクが耐えねばならないもう 1 つは、攻撃者が飛行中に書き換えて弱い選択を強いてくることだ。書き換えるフィールドを選び、2 つのバージョンを比べよう:

書き換えなし

CertificateVerify はここまでの全ハンドシェイクバイトのハッシュへの署名だ。どこを書き換えてもクライアントのトランスクリプトがサーバのものと食い違い、署名が検証できない ——decrypt_error (51)、アプリケーションのバイトが動く前にだ。TLS 1.2 は鍵交換パラメータしか署名せず、署名されない残りが FREAK と Logjam の穴だった。

最後に、これらすべてが応答する機械にいくらかかるか。接続レートを設定し、フルハンドシェイクと再開を比べてみよう:

毎秒 1,000 ハンドシェイク

毎秒 1 万ハンドシェイクまで動かすと、フルハンドシェイクは 1.09 コア、再開は 0.80 コア。再開が節約するのは署名であって鍵交換ではない。既定の psk_dhe_ke は前方秘匿性のため ECDHE を回すからだ。psk_ke はそれを飛ばしてほぼ無料になり、前方秘匿性を捨てる。

05

鍵を名前に結びつける

誤った機械への完璧に暗号化された通路に価値はない。だから公開鍵をホスト名に縛るものが要る。PKI の各部品はその縛りを検査可能にするためにある。

証明書は署名された主張だ。この鍵はこれらの名前のもので、この日付まで、これらの用途に使える。クライアントは各条項を確認する。条項は交換できず、どれで落ちたかで別のバグになる。

TLS クライアントが実際に検証する 6 つを歩き、それぞれが何を拒むか読もう:

Subject Alternative Name

Common Name がリストに無いことに注目してほしい。Chrome は 2017 年に読むのをやめ、他も追随した。ホスト名はSubject Alternative Name 拡張にあり、他のどこにも無い。 CN が正しく SAN が空の証明書は落ちるが、エラーが指すのは運用者が一度も編集したことのないフィールドだ —— だから毎回半日が飛ぶ。

いちばん間違えられるのがホスト名の照合で、ワイルドカードは見た目どおりの意味を持たないからだ。ホスト名を選び、SAN のエントリと突き合わせてみよう:

example.com へ接続

ワイルドカードはちょうど 1 ラベルに一致する。*.example.com は api.example.com を覆い、a.b.example.com は覆わず、そして —— これが驚かれる —— example.com 自身も覆わない。4 つ目のホスト名が攻撃だ。api.example.com.evil.net の末尾は evil.net であり、照合が部分文字列探索ではなく右からラベル単位で歩く理由がここにある。

リーフ単独では何も証明しない。クライアントが既に持っている鍵まで届く必要がある。チェーンを上り、次に中間証明書を外してみよう:

リンク 1 / 3

クライアントはルートしか同梱しないので、サーバは中間証明書をすべて送らねばならない。 1 枚落とすのは最も多い設定ミスで、しかも非対称に壊れる。ブラウザは AIA 拡張から発行者を取って回復するが、curl と Java と Go はしない。Chrome では動き CI では落ちる。壊れ方として最悪だ。

中間証明書があるのは、ルートの秘密鍵が金庫の中でオフラインにあり、署名するのが十年に一度程度だからだ。リーフはその逆である —— 90 日の証明書の上で「今日」をドラッグしてみよう:

0 日目 / 90 日。横にドラッグして今日を動かす;矢印キーは 1 日ずつ、Home で戻る
0 日目 / 90 日

更新ウィンドウが 60 日目に開くのを見てほしい。30 日の余裕があるのは、更新が無人で走り静かに失敗するジョブだからだ —— DNS の変更、失効した ACME アカウント、ファイアウォール規則 —— そして 30 日は誰かが気づくのにかかる時間である。90 日を過ぎるとクライアントはcertificate_expired (45) で拒否する。良い方のケースだ。

短い有効期間は衛生管理ではなく、失効という問題への答えそのものである。機構を選び、鍵の盗難が報告されてからの時間をドラッグしよう:

失効から 0 時間

上 2 本のバーが同じ長さであることに注目してほしい。素の OCSP はソフトフェイルする。レスポンダに届かなければクライアントはそのまま続行するので、 HTTP リクエストを 1 本止められる攻撃者は 90 日まるごとを手に入れる。ステープリングは取得をサーバ側へ移してそれを塞ぎ、6 日の証明書はそもそも失効を必要としないことで塞ぐ。CA/Browser Forum が 2025 年に最長有効期間を 2029 年までに 47 日へ縮めると決めたのはこのためだ。

最後の変種は、同じ検査を逆方向にも走らせることである。相互に切り替えて両方を歩いてみよう:

クライアントがサーバを検証

サーバ認証だけの TLS では、サーバは接続が私的だとは分かるが相手が誰かは分からない。だから bearer トークンに頼り、ログの中のそれは資格情報そのものになる。mTLS はクライアントの秘密鍵を資格情報にする。ヘッダから抜き取れず、メッシュのサイドカーが 1 時間ごとに回す。代償はもう 1 つ PKI を運用することだ。

06

レコード層 —— バイトが実際に住む場所

ハンドシェイクは 1 ミリ秒で終わる。以後の全バイトはレコード層を通り、その 2 つのパラメータは設定の顔をしたレイテンシとスループットの判断だ。

レコードは暗号化の単位だ。送信側は 1 本を満たし、封をし、ソケットへ書く。受信側は全部が届くまで何もできない。最後の一節は細部ではなくレイテンシの予算である。

まず 1 本のレコードがワイヤ上でどう見えるか、送信側が何を隠せて何を隠せないかを見よう。パディングを足して長さフィールドを見ていてほしい:

パディング 0 バイト

ヘッダの色が決して変わらないことに注目してほしい。型、バージョン、長さは AEAD の外にあるので、観測者は各レコードが運んだバイト数を常に知る —— パディングは数字を動かせても消せない。TLS 1.3 は本当のコンテンツタイプを暗号文の内側に隠すので、外側の型はハンドシェイクメッセージでも application_data と書いてある。

送信側が選ぶサイズは取引であり、その両端が同時に画面に出ている。レコードサイズをドラッグし、動く 3 つの数を読もう:

16 KB のレコード。横にドラッグしてレコードサイズを変える;矢印キーは半分と倍、Home で戻る
16 KB のレコード

16 KB では 1 MB の応答が 64 レコードと1.4 KB のオーバーヘッド —— 0.13% だ。64 バイトのレコードまで下げると同じ応答が 16,384 レコードと 352 KB、税率 34% になる。だが痛いのは大きいレコードのほうだ。10 Mbps の回線では 16 KB レコードの中身は13.1 ミリ秒分が届くまで何も復号できない。ストリーミングサーバが小さいレコードを、バルク転送が最大値を使うのはこのためである。

1 本の鍵も永遠には使えず、その上限は方針ではなく算術だ。AEAD とレコードサイズを選び、その鍵が守れるデータ量を読もう:

16 KB のレコード

RFC 8446 §5.5 は AES-GCM を鍵あたり 224.5 レコード —— 約 2,370 万 —— に制限する。超えるとカウンタの誕生日限界が無視できなくなるからだ。16 KB レコードなら362 GBで、10 ギガビットの接続なら約 5 分で到達する。KeyUpdate はハンドシェイクなしで片方向を回す。 ChaCha20-Poly1305 に上限は無い。

以上のどれもトラフィックの形は隠さない。観測者がなお学べるものを、 Encrypted Client Hello の有無で歩いてみよう:

宛先 IP

6 つのうち 4 つが TLS 1.3 でも生き残る。面白いのはホスト名だ。SNI は平文で運ばれる —— 多数の証明書を持つサーバが鍵を持たない段階で正しい 1 枚を選べるようにするためで、だからここが各国のファイアウォールのフィルタ対象になる。ECH は DNS に公開された鍵でこれを暗号化し、残るのは宛先 IP だけ。CDN の後ろでは 1 つの IP が数百万サイトを提供する。バイト数とタイミングはどちらでも残る。

07

どう壊れ、いくらかかるか

そらで考えるべきことが 2 つ。TLS の失敗が呼び出し側にどう届くか、そしてCPU が実際どこへ行くか。

失敗はほぼすべて設計として大声だ —— プロトコルに「無視して続行」は無い。危険なのはプロトコルが見ない失敗で、すべて設定である。

実際のサービスがぶつかる 6 つを歩き、それぞれが何を返すか読もう:

ホスト名が SAN リストにない

レビューで探すのは最後の行だ。curl -k、verify=False、InsecureSkipVerify: true —— ハンドシェイクは成立し、トラフィックは本当に暗号化され、alert もログ行も出ない。他の行はすべてアプリケーションのバイトが動く前に中止する。

2 つ目の静かな失敗は、誰も見直さないバージョンポリシーである。サーバが受け入れる最低バージョンをドラッグし、既知の攻撃が消えていくのを見よう:

SSL 3.0 — 到達可能な攻撃 6 件

TLS 1.2 でも 2 件残ることに注目してほしい —— CBC スイート経由の LUCKY13 と、小さい有限体群経由の Logjam だ。1.2 はそれらを設定で外させるだけで、削除はしない。TLS 1.3 でゼロになる理由は別で、アルゴリズムが仕様に存在しないからである。

alert が出ないものがもう 2 つある。TLS に到達しないからだ。HTTP から HTTPS へのリダイレクトは、HSTS がプリロードされていない限り最初のリクエストをcookie ごと平文で残す。平文 HTTP 上のアプリ層 AES は §02 で歩いたバグを、監査なしで作り直す。

コストの問題は評判より単純だ。回線速度を設定し、そのコアに AES 命令があるかを選ぼう:

1 Gbps のトラフィック

1 Gbps では AES-128-GCM が 0.02 コア。10 Gbps まで動かすと 0.24 になる。命令を取り上げると同じトラフィックが 6.94 コア、ChaCha20-Poly1305 は 1.14 で済む —— スマートフォンが ChaCha を、サーバが AES を暗号リストの先頭に置く理由だ。