HTTPSとTLSの仕組みを徹底解説:ハンドシェイクと証明書
SSL証明書を追加し、ドメインをサーバーに向ければ、鍵アイコンが表示されます。ほとんどの開発者はそこで作業を終えます。しかしある日、APIクライアントがUNABLE_TO_VERIFY_LEAF_SIGNATUREで失敗したり、古いAndroidでモバイルアプリが証明書を拒否したり、ロードバランサーがTLSを終端してアプリがすべてを平文HTTPとして認識したりします。その時初めて、TCP接続と最初のHTTPリクエストの間で実際に何が起こっているのかを知る必要が出てきます。
この記事では、HTTPSとTLSが実際にどのように機能するかを解説します。ハンドシェイクが何をするのか、証明書がどのように検証されるのか、TLS 1.3で何が変わったのか、そして実際に遭遇するエラーの診断方法を説明します。
HTTPSが実際に追加するもの
HTTPSはTLS接続上で送信されるHTTPです。TLSは3つの保証を提供します:
- 機密性 — トラフィックはハンドシェイク中にネゴシエートされた対称鍵で暗号化されます。
- 完全性 — 各レコードには認証タグが付き、改ざんが検出されます。
- 認証 — サーバーは、要求したホスト名に一致する証明書の秘密鍵を制御していることを証明します。
このリストにないものに注意してください:クライアントはデフォルトでは認証されません。TLSはサーバーを認証し、アプリケーションがユーザーを認証します。
TLS 1.2ハンドシェイクの手順
TLS 1.2は古いインフラや多くの企業プロキシで依然として一般的なので、理解する価値があります。完全なハンドシェイクは次のようになります:
- ClientHello — クライアントはサポートするTLSバージョン、ランダム値、暗号スイートのリスト、(該当する場合は)SNIホスト名を送信します。
- ServerHello — サーバーはバージョンと暗号スイートを選択し、自身のランダム値と証明書チェーンを返します。
- 鍵交換 — ECDHEでは、サーバーは一時的な公開鍵を送信し、クライアントも自身の公開鍵を送信します。両者は同じ共有秘密を導出します。サーバーは証明書の秘密鍵でハンドシェイクトランスクリプトに署名します。
- Finished — 両側がハンドシェイクトランスクリプトのMACを送信し、何も改ざんされていないことを確認します。
- アプリケーションデータ — HTTPリクエストとレスポンスが暗号化されて流れます。
重要な詳細は一時的鍵交換です。ECDHEでは、接続ごとに新しい鍵ペアが生成されます。攻撃者が何年分ものトラフィックを記録し、後でサーバーの秘密鍵を盗んだとしても、過去のセッションを復号することはできません。この特性は前方秘匿性(forward secrecy)と呼ばれ、静的RSA鍵交換が非推奨となった主な理由です。
TLS 1.3が変えたもの
TLS 1.3はハンドシェイクを1往復に簡素化し、レガシーアルゴリズムを完全に削除しました。
| 項目 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 往復回数(完全ハンドシェイク) | 2 | 1 |
| 鍵交換 | RSAまたはECDHE | ECDHEのみ(前方秘匿性) |
| 暗号スイート | 暗号、MAC、鍵交換が別々 | AEADスイートのみ、例:TLS_AES_128_GCM_SHA256 |
| 静的RSA、CBC、RC4 | 許可 | 削除 |
| セッション再開 | セッションID / チケット | PSKと0-RTTオプション |
TLS 1.3では、クライアントが鍵交換グループを推測し、最初のメッセージで自身のシェアを送信するため、サーバーは即座に鍵合意を完了できます。証明書は依然として送信されますが、暗号化されるようになったため、受動的な観察者はどのホスト名が提供されているかを見ることができなくなりました。Encrypted Client Helloが使用されていない限り、ClientHelloのSNIのみがそれを漏らします。
0-RTT再開により、再訪するクライアントは最初のフライトでアプリケーションデータを送信できます。高速ですが、0-RTTデータはリプレイ可能であるため、支払いなどの非冪等リクエストには決して使用しないでください。
証明書とチェーン検証
証明書は公開鍵をホスト名にバインドします。検証はチェーンをたどる作業です:
- サーバーはリーフ証明書と中間証明書を送信します。
- クライアントは信頼ストアで信頼されたルートを探し、チェーンをさかのぼって各署名を検証します。
- ホスト名をSubject Alternative Name(SAN)フィールドと照合します — 現代のクライアントではCommon Nameは無視されます。
- 有効期間と失効ステータス(CRLまたはOCSP、多くの場合ステープリング)を確認します。
unable to verifyエラーの最も一般的な原因は、中間証明書の欠落です。ブラウザは中間証明書をキャッシュしてこの問題を覆い隠すことがありますが、コマンドラインツールやモバイルアプリは厳密に失敗します。常に完全なチェーンを提供してください。リーフを先頭に、次に中間証明書を送信し、ルートは決して送信しないでください。
OpenSSLを使った実践的デバッグ
何かが壊れたときは、クライアントを責める前に、サーバーが実際に何を送信しているかを調べましょう。
# サーバーが提示する証明書チェーンを表示
openssl s_client -connect example.com:443 -servername example.com -showcerts
# TLS 1.3のみをネゴシエートし、選択された暗号を表示
openssl s_client -connect example.com:443 -tls1_3
# 証明書の有効期限を1行で確認
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates -subject -ext subjectAltNames_clientがverify error:num=21:unable to verify the first certificateを報告した場合、チェーンが不完全です。ホスト名の不一致を報告した場合は、SANリストを確認してください。ワイルドカード証明書は1つのラベルのみをカバーするため、*.example.comはapi.example.comに一致しますが、a.b.example.comには一致しません。
よくある設定ミス
- 混在コンテンツ — HTTPSページが
http://のスクリプトや画像を読み込む。ブラウザはアクティブな混在コンテンツをブロックし、パッシブなコンテンツを自動的にアップグレードする場合があります。 - ロードバランサーでのTLS終端 — バックエンドは平文HTTPを受信します。
X-Forwarded-Protoを転送し、既知のプロキシからのみ信頼してください。そうしないと、アプリが誤ったリダイレクトURLを生成する可能性があります。 - 弱いプロトコルバージョンが有効 — TLS 1.0と1.1は無効にすべきです。TLS 1.2と1.3のみを目指してください。
- HSTSがない —
Strict-Transport-Securityヘッダーがないと、最初のリクエストがダウングレードされる可能性があります。短いmax-ageから始め、確信が持てたら増やしてください。 - 本番環境での自己署名証明書 — 内部テストには問題ありませんが、ユーザーやクライアントに警告を無視する習慣をつけさせます。
妥当なサーバー設定
Nginxの場合、現代的なベースラインは次のようになります:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;TLS 1.3ではサーバーに暗号を選択させてください — スイートはすべて強力なので、クライアントの優先順位で問題ありません。チケットキーをローテーションしない限り、セッションチケットはオフにしてください。チケットキーが漏洩すると、再開が危険にさらされる可能性があります。
FAQ
HTTPSはホスト名を暗号化しますか?
デフォルトでは暗号化しません。ClientHelloのSNI拡張は、サーバーが提示すべき証明書を知るために平文で送信されます。Encrypted Client Hello(ECH)はそれを隠しますが、採用はまだ限定的です。パスとクエリ文字列は常に暗号化されます。
証明書がブラウザでは機能するのに、curlやモバイルアプリで失敗するのはなぜですか?
ほとんどの場合、中間証明書の欠落です。ブラウザは中間証明書を独自に取得またはキャッシュすることがよくありますが、より厳格なクライアントはサーバーが完全なチェーンを送信することを要求します。openssl s_client -showcertsで確認してください。
TLS 1.3は常にTLS 1.2より高速ですか?
新規接続の場合は、はい — 2往復ではなく1往復です。再開接続では差は縮まり、0-RTTは非冪等リクエストのリプレイリスクを犠牲にしてTLS 1.3をさらに高速にできます。
TLS変更後にWebサーバーを監査する場合、ログ分析は実際の影響を明らかにすることがよくあります:壊れたリダイレクト、予期しない4xxの急増、HTTPにまだアクセスしているクライアントなどです。Nginxログアナライザーにアクセスログを投入すると、ステータスコードの分布を確認し、HTTPSへのアップグレードを完了しなかったクライアントを特定できます。