HTTPSとTLSの仕組みを徹底解説:ハンドシェイクの全手順
証明書を追加してブラウザに鍵アイコンが表示されたら、それで終わりにする。ところがある日、APIクライアントがSSL_ERROR_RX_RECORD_TOO_LONGで失敗したり、モバイルアプリが証明書チェーンを拒否したり、HSTSを有効にした後にリダイレクトループが発生したりします。突然、HTTPSがブラックボックスに感じられます。
この記事ではその箱を開けます。TLSハンドシェイクで実際に何が起きているのか、なぜ証明書が存在するのか、共通鍵暗号と公開鍵暗号がそれぞれどこで使われるのか、そして本番環境で現実に遭遇する障害をどうデバッグするのかを順に解説します。
HTTPSの実体とは
HTTPSは独立したプロトコルではありません。TLSで暗号化された接続上で送られる、ただのHTTPです。URLスキームが変わり、デフォルトポートが80から443に変わるだけで、メソッド、ヘッダー、ステータスコード、Cookieなどそれ以外の振る舞いはすべて同じです。
TLSはTCPの上に次の3つの保証を追加します。
- 機密性 — 中間者は通信内容を読めません。
- 完全性 — 転送中の改ざんは検知されます。
- 認証 — クライアントは、そのドメインを所有するサーバーと通信していることを検証できます。
過小評価されがちなのは3番目の保証です。認証のない暗号化は無意味です。通信を傍受できる攻撃者は、単に自分の鍵を提示すればよいからです。
TLS 1.3ハンドシェイクを順を追って解説
TLS 1.3はTLS 1.2と比べてハンドシェイクを劇的に簡素化しました。ブラウザとサーバーの間の流れは次のとおりです。
- ClientHello — クライアントは対応するTLSバージョン、暗号スイート、ランダム値、そしてDiffie-Hellmanグループ(例えばX25519)の鍵共有を送信します。
- ServerHello — サーバーは暗号スイートを選択し、自身の鍵共有を送信します。これで双方が同じ共有秘密を導出できます。
- 暗号化された拡張と証明書 — サーバーは証明書チェーンを送信します。これはすでに導出された鍵で暗号化されています。
- Finished — 双方がハンドシェイクトランスクリプトのMACを送信し、何も改ざんされていないことを証明します。
- アプリケーションデータ — 確立されたセッション上でHTTPリクエストとレスポンスが流れます。
TLS 1.3ではフルハンドシェイクは1ラウンドトリップで完了し、セッション再開ではゼロラウンドトリップに達することができます。TLS 1.2ではアプリケーションデータが流れる前に2ラウンドトリップ必要でした。
鍵の導出後に証明書が送られる理由
TLS 1.3では証明書が暗号化されます。つまり受動的な傍受者は、証明書だけからクライアントがどのドメインにアクセスしているかを知ることすらできず、情報漏えいが軽減されます。同時に、最初のパケットに平文の証明書があることを前提とするデバッグツールは混乱して見えることになります。
共通鍵暗号と公開鍵暗号
よくある誤解は、HTTPSがすべてをサーバーの公開鍵で暗号化しているというものです。そうではありません。公開鍵暗号は低速でサイズ制限もあるため、共有秘密の確立にのみ使われます。
| 段階 | 暗号方式 | 目的 |
|---|---|---|
| 鍵交換 | 公開鍵暗号(ECDHE) | 共有秘密を送信せずに合意する |
| 認証 | 公開鍵署名 | サーバーが証明書の鍵を所有していることを証明する |
| 一括データ | 共通鍵暗号(AES-GCMまたはChaCha20-Poly1305) | HTTP通信の高速な暗号化と完全性 |
Diffie-Hellman鍵交換には前方秘匿性と呼ばれる貴重な性質があります。サーバーの秘密鍵が後で漏えいしても、記録された過去のセッションは復号できません。セッション鍵がその長期鍵から導出されていないからです。
証明書の中身
X.509証明書は公開鍵をドメイン名に結び付けます。含まれる内容は次のとおりです。
- サブジェクト(ドメイン)とサブジェクト代替名(SAN)。ブラウザが実際に確認するのはこちらです。
- 公開鍵とそのアルゴリズム。
- 有効期間。
- 発行者と、その発行者による署名。
ブラウザは固定されたルート認証局のセットを信頼します。サーバーは中間証明書を送信し、ブラウザはリーフ証明書から信頼されたルートまでチェーンを構築します。中間証明書が欠けている場合、デスクトップブラウザは中間証明書をキャッシュしているため動作することがありますが、モバイルクライアントやコマンドラインツールは失敗します。これは本番環境で最もよくあるTLSのミスです。
実践的なデバッグ手順
HTTPSが不調なときは、外側から内側へと調べていきます。
- チェーンを確認する —
openssl s_client -connect example.com:443 -servername example.com -showcertsで確認します。サーバーがリーフだけでなく完全なチェーンを送信していることを確かめます。 - 有効期限を確認する — 期限切れの証明書は回避策のない致命的な失敗を引き起こします。
- ホスト名を確認する — SANリストには、クライアントが要求する正確なホスト名が含まれている必要があります。
wwwプレフィックスの有無も含めて確認します。 - プロトコルバージョンを確認する — レガシークライアントがTLS 1.0しかサポートしておらず、サーバーがTLS 1.2以上を要求している場合、HTTPの前にハンドシェイクが失敗します。
- リダイレクトを確認する — HTTPSからHTTPへ、そして再びHTTPSへというリダイレクトはループを生みます。アプリケーションコードではなくサーバー設定で修正します。
# 証明書チェーンとネゴシエートされたプロトコルを確認する
openssl s_client -connect example.com:443 \
-servername example.com -showcerts < /dev/null 2>&1 | \
openssl x509 -noout -subject -issuer -dates
# 特定のTLSバージョンをテストする
openssl s_client -connect example.com:443 -tls1_2 < /dev/nullNginxでは、ssl_certificateディレクティブはリーフ証明書の後に中間証明書を続けたファイルを、その順序で指す必要があります。順序を誤って連結すると、見逃しやすいチェーンエラーが発生します。
HSTSと設定を誤ったときの代償
HTTP Strict Transport Security(HSTS)は、指定された期間そのドメインでHTTPSを使うようブラウザに指示します。強力である一方で容赦がありません。ブラウザが長いmax-ageを一度見てしまうと、後でサーバー側でHTTPSを無効にしても平文HTTPを拒否するようになります。
短いmax-ageから始め、すべてが動作することを確認してから増やしてください。includeSubDomainsは、すべてのサブドメインが有効なHTTPSを提供していることを確認してから追加します。プリロードは事実上永続的なので、確信が持てたときだけ申請してください。
よくある設定ミス
- 混合コンテンツ — HTTPSページがHTTP経由でスクリプトや画像を読み込むケース。ブラウザはアクティブな混合コンテンツをブロックし、パッシブなものには警告を出します。
- 弱い暗号スイート — もはや不要な互換性のためにCBCモードの暗号や古いプロトコルバージョンを有効にしたままにしているケース。
- OCSPやCRLの処理漏れ — 失効確認は実際には信頼性が低く、短命証明書のほうが堅牢な答えです。
- 証明書自動化の抜け — 更新は動くものの、サーバーに新しい証明書を読み込ませるリロードのステップが欠けているケース。
証明書の更新はカレンダーのリマインダーではなく、デプロイパイプラインとして扱いましょう。更新に人手が必要なら、遅かれ早かれ最悪のタイミングで失敗します。
よくある質問
HTTPSはアクセス先のドメイン名を暗号化しますか?
ドメインはTLSのSNI拡張に含まれ、通常はネットワーク上で見えます。パスやクエリ文字列は暗号化されますが、ネットワークの傍受者は通常、どのホストに接続したかを知ることができます。
ブラウザでは動くのにcurlやモバイルアプリでは失敗するのはなぜですか?
ほとんどは中間証明書の欠落です。ブラウザは以前のアクセスでの中間証明書をキャッシュしていることが多い一方、curlや多くのモバイルHTTPスタックはサーバーが完全なチェーンを送信することを要求します。
TLS 1.3は常にTLS 1.2より優れていますか?
現代のクライアントにとっては、はい。ラウンドトリップが少なく、暗号スイートのセットが小さく、デフォルトで前方秘匿性があります。1.3をネゴシエートできないクライアントにまだ対応する必要がある場合にのみ、TLS 1.2を有効にしておきましょう。
TLSを終端するサーバーのログを確認する必要があるときは、Nginxログアナライザーをさっと通すことで、ハンドシェイクの失敗、リダイレクトループ、異常なクライアントの振る舞いを障害になる前に見つけられます。