⑥では古典的なSSLのRSA鍵輸送を、⑦ではTLSの歴史と、使い捨ての鍵で共有秘密を作る仕組みを追いました。今回は、そのやり取りを実際の接続から見てみます。自宅ラボの検証用サーバーへOpenSSLで接続し、メッセージの名前と順序、選ばれた方式を確かめます。同じRSA証明書を使い、TLS 1.2のRSA鍵輸送とECDHEの違いも比べます。

暗号化と認証

 暗号化と認証⑧

 OpenSSLで、TLSの接続と鍵交換を追いかける

⑥では古典的なSSLのRSA鍵輸送を、⑦ではTLSの歴史と、使い捨ての鍵で共有秘密を作る仕組みを追いました。今回は、そのやり取りを実際の接続から見てみます。

自宅ラボの検証用サーバーへOpenSSLで接続し、メッセージの名前と順序、選ばれた方式を確かめます。同じRSA証明書を使い、TLS 1.2のRSA鍵輸送とECDHEの違いも比べます。

まずは、それぞれのメッセージが何のために送られるのかを押さえます。鍵を作り分ける理由や、証明書と署名が何を確かめるのかは、次回⑨で詳しく追います。

手元で再現するためのサーバー準備は、記事の後半にまとめています。まず結果を読み、必要に応じて試せる構成です。

HTTPSの前に、まずTCPでつながる

ブラウザーからWebサーバーへ接続するときには、TLSのメッセージを運ぶための通信路を用意します。

公開サイトなら、通常はホスト名から接続先を調べ、たとえばbricklayer.work:443へ接続します。

今回は、TCPで接続し、その上でTLSの手続きを行って、HTTP/1.1のリクエストを送る流れを追います。

ただし、TCPでつながっただけでは、相手の証明書を確認したことにも、通信を暗号化したことにもなりません。

その上で、TLSのハンドシェイクが始まります。

ClientHelloとServerHelloで、何を決めているのか

ブラウザーが最初に送る申し出のメッセージをClientHello(クライアント・ハロー)、サーバーが選択結果を返すメッセージをServerHello(サーバー・ハロー)と呼びます。Helloという名前ですが、単なる挨拶ではありません。

ここではTLS 1.3の場合を見ます。ClientHelloでは、クライアントが対応している方式や、今回の鍵交換に必要な公開情報をまとめて送ります。

主なものを整理すると、こうなります。

情報何のために送るか
対応するTLSのバージョンどのバージョンで接続するか決める
暗号スイートの候補通信を保護する暗号と、鍵導出などに使うハッシュを決める
対応する署名方式相手の署名を、どの方式で検証できるか伝える
鍵交換の方式とkey_share双方が通信に使う秘密を作るための公開情報を渡す
SNI(接続したいサイト名を伝える仕組み)のホスト名サーバーへ、どの名前のサイトに接続したいか伝える

サーバーはその内容を受け取り、使うTLSのバージョン、暗号スイート、鍵交換の公開情報などをServerHelloで返します。

この段階で、方式の選択と鍵交換が進みます。

ちなみに、TLS 1.3の暗号スイート名は、署名方式や鍵交換方式まで全部を一つの名前に詰め込んだものではありません。そこは別に選びます。[1]

この後の実験では、この分離を接続結果から確かめます。

OpenSSLで、接続の順序を見てみる

まず、ブラウザーの裏で行われるやり取りを、手元で見える形にしてみます。

⑤ではopenssl s_clientに-showcertsを付け、送られてきた証明書を見ました。

今回は、そこへ-msgと-stateを付けます。

-msgはプロトコルのメッセージ、-stateは接続処理の状態を表示するオプションです。

まずは結果を揃えやすいように、手元に検証用サーバーを用意します。サーバーの再現手順は記事の後半にまとめました。

クライアント側は、同じ作業ディレクトリから次のように接続します。

Bash
printf 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' |
openssl s_client \
  -connect 127.0.0.1:18443 \
  -servername localhost \
  -verify_hostname localhost \
  -CAfile lab-ca.crt \
  -verify_return_error \
  -tls1_3 \
  -msg \
  -state \
  -ign_eof

接続するだけでなく、HTTPのリクエストも一つ送っています。

各オプションの意味は、次のとおりです。[2]

オプション意味
-connect 127.0.0.1:18443接続先のIPアドレスとポート番号。今回は自分のPCの18443番へ接続する。
-servername localhostサーバーに「localhostという名前のサイトへ接続したい」と伝える。SNIの指定。
-verify_hostname localhost受け取った証明書が、localhostという名前に対応しているか検証する。
-CAfile lab-ca.crt証明書の検証に使う、実験用CAの証明書を指定する。
-verify_return_error証明書の検証に失敗したら、接続を中止する。
-tls1_3使用するバージョンをTLS 1.3に限定する。
-msg送受信したTLSメッセージの種類や、内容の16進数表示を出す。
-state接続準備がどの段階まで進んだかを表示する。
-ign_eof入力が終わっても、すぐに接続を閉じず、サーバーからの応答を受け取る。

SNI(Server Name Indication:サーバー名表示)は、接続したいサイト名をサーバーへ伝える仕組みです。

-servernameはサーバーに名前を伝える指定、-verify_hostnameは証明書の名前を確かめる指定です。似ていますが、役割は別です。

先頭のprintfは、送信するHTTPリクエストを作っています。GET / HTTP/1.1でトップページを要求し、Host: localhostでサイト名を指定、Connection: closeで応答後に接続を閉じるよう求めます。\r\nはHTTPで使う改行で、最後の空行がヘッダーの終わりを示します。

そのリクエストを、|でopenssl s_clientへ渡し、TLSで保護した接続の中へ送ります。

今回のコマンドでは、まず127.0.0.1:18443へTCPで接続し、その接続の中でTLSの手続きを始めています。

-CAfileで、この実験で信用するCAの証明書を指定します。OS全体の信頼済みストアへ登録する必要はありません。

また、-verify_return_errorは省略しません。s_clientは診断用の道具なので、既定では証明書の検証エラーを表示しても、そのまま処理を続ける場合があるためです。[2]

TLSで、通信を始めるために方式を相談し、鍵を共有し、相手を確かめる一連の手続きをハンドシェイクと呼びます。

ログには、証明書を送るCertificate、今回のやり取りに結び付くサーバーの署名を送るCertificateVerify、ここまでのやり取りを照合する確認値を送るFinishedも登場します。まずは名前と大まかな役割を押さえ、仕組みは次回⑨で詳しく見ます。

OpenSSL 3.0.13で確認したログから、初回ハンドシェイクのメッセージを拾い、短い説明を添えると次の順になります。>>>はクライアントから送信、<<<はサーバーから受信です。矢印の行は、ログには出ない双方の計算を補足しています。

なお、今回使うs_client -msgでCertificateの内容が読めるのは、このクライアント自身がTLSの当事者だからです。通信路を横から眺めれば、同じ内容がそのまま読める、という意味ではありません。

その後、簡易HTTPサーバーからHTTP/1.0 200 okが返りました。

⑦で言葉で追った流れが、ここではメッセージの順序として見えます。

Certificateより前に、ClientHelloとServerHelloで鍵を作るための公開情報を交換しています。

先に接続準備を暗号化する鍵を作り、その鍵で保護された証明書と署名が届いています。

⑥のRSA鍵輸送の説明では、証明書を確認した後に、ブラウザーが鍵の材料を送る流れでした。⑦で見た公開情報の交換と、その後に証明書や署名が届く順序が、このログにも現れています。ここからは、実際に選ばれた方式や接続結果を確かめます。

証明書を確かめる前に、接続準備の暗号化が始まっています。暗号化しただけで相手を信用できるのでしょうか。また、一つの共有秘密から、なぜ用途の違う鍵を作るのでしょうか。この二つの疑問は、次回⑨で追います。

RSAの証明書、X25519の鍵交換、AESの通信

今回使ったサーバー証明書の公開鍵は、2048bitのRSA鍵です。

一方、接続の結果から重要な項目を拾うと、こうなりました。

Peer signature type: RSA-PSS
Server Temp Key: X25519
Verification: OK
Cipher: TLS_AES_256_GCM_SHA384

RSAの証明書なのに、通信の暗号はAES。途中にはX25519まで出てきます。

それぞれ、担当している仕事が違います。

今回の方式担当している仕事
RSA-PSSサーバーがCertificateVerifyで作る署名
X25519今回の鍵交換で共有秘密を作る計算
AES-256-GCMTLSレコードのデータを暗号化し、改ざんを検出する
SHA-384この暗号スイートでの鍵導出やハンドシェイクの照合に使うハッシュ

「RSA証明書だから、通信の暗号も鍵交換も全部RSA」という話ではありません。

昔のTLSの説明には、「共通鍵の材料をRSAの公開鍵で暗号化して送る」というものもあります。それはTLS 1.2などで使われたRSA鍵輸送の説明です。TLS 1.2にもECDHEを使う方式はありますし、TLS 1.3ではRSA鍵輸送は使いません。[1][3]

今回のように一時的な鍵を使う鍵交換には、証明書の長期秘密鍵とは別に、今回の通信の秘密を作る意味があります。

一時秘密鍵が安全に破棄されていれば、後から証明書の秘密鍵だけを入手しても、記録済みの通信から過去の共有秘密を計算できる構成にはなっていません。これが前方秘匿性という性質につながります。

③と④でRSAの計算へ潜ったことも、無駄にはなっていません。

ただ、その数学がTLSのどの役割に使われているかを、区別して見る必要がありました。

ここから、HTTPのやり取りが始まる

今回の接続では、ハンドシェイクの後に、先ほどのHTTPリクエストが送られました。

GET / HTTP/1.1
Host: localhost
Connection: close

アプリケーションから見れば、普通のHTTPです。

そのデータを、TLSがアプリケーション通信用の鍵で保護して送ります。

ここで使う鍵は、証明書などを運んだハンドシェイク用の鍵とは別です。

今回の実験では、AES-GCMが選ばれています。①でC#から動かした、暗号化と改ざんの検出を一緒に行う方式が、ここで通信の保護に使われています。

Webページの内容をRSAで一文字ずつ暗号化しているわけではありません。

最初に鍵交換と相手の確認を行い、その結果から作った鍵で、実際のデータを効率よく保護しています。

ログの後半にはNewSessionTicketも現れました。これは、今後の接続を再開するための情報です。最初の接続のCertificateやCertificateVerifyと同じ役割ではありません。

通信を終えるときにも、TLSの手順がある

今回のHTTPリクエストにはConnection: closeを付けています。

簡易HTTPサーバーは応答を返し、その接続を閉じます。TLSのログでは、終了を知らせるclose_notifyも確認できます。

TLSには、データを守って送るだけでなく、「こちらからの送信はここで終わる」と相手へ知らせる手順もあります。

ただTCPの接続が突然切れただけなのか、TLSとして送信を終えたのかは、同じではありません。[1]

普段は、HTTPクライアントやTLSライブラリがこの辺りも扱っています。だからアプリケーションから見ると、ページを受け取って終わっただけに見えます。

でもログを眺めると、接続の入口にも出口にも、手続きがあることが分かります。

同じRSA証明書で、TLS 1.2の二つの方式を比べる

説明だけで終わらせず、手元でも違いを見てみます。

同じ2048bitのRSA鍵を持つサーバー証明書を使い、RSA鍵輸送、ECDHE_RSA、TLS 1.3の三つをOpenSSL 3.0.13で比較しました。いずれも証明書の検証に成功し、HTTPの応答まで受け取れました。

RSA鍵輸送も使うため、この実験の証明書にはdigitalSignatureとkeyEnciphermentの両方を指定します。後半のサーバー準備に、その設定を載せています。[3]

比較するRSA鍵輸送は、2026年のRFC 10015で使用しないことが要求された方式です。廃止された手順を理解するため、接続先を127.0.0.1に限定して実験します。[4]

まず、RSAで秘密の材料を渡す

後半の準備で作ったファイルを使い、比較用サーバーを別のポートで起動します。

Bash
openssl s_server \
  -accept 127.0.0.1:18444 \
  -cert server.crt \
  -key server.key \
  -tls1_2 \
  -cipher AES256-GCM-SHA384 \
  -no_ticket \
  -www

別のターミナルから、クライアントも同じ方式へ固定して接続します。

Bash
printf 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' |
openssl s_client \
  -connect 127.0.0.1:18444 \
  -servername localhost \
  -verify_hostname localhost \
  -CAfile lab-ca.crt \
  -verify_return_error \
  -tls1_2 \
  -cipher AES256-GCM-SHA384 \
  -msg \
  -state \
  -ign_eof

OpenSSLのAES256-GCM-SHA384は、ここではTLS_RSA_WITH_AES_256_GCM_SHA384の名前に対応します。[5]

初回ハンドシェイクの名前を拾うと、こうなりました。ChangeCipherSpecなどは、この一覧には含めていません。

>>> ClientHello
<<< ServerHello
<<< Certificate
<<< ServerHelloDone
>>> ClientKeyExchange
>>> Finished
<<< Finished

TLS 1.3で見たCertificateVerifyがありません。鍵交換の公開情報も、Helloのkey_shareとしては出てきません。

代わりに、ClientKeyExchangeの中へ、RSAで暗号化されたpremaster secretが入っています。

今回のRSA鍵は2048bitなので、取り出したRSA暗号文は256バイトでした。それを接続終了後に検証用のserver.keyで復号すると、48バイトのpremaster secretを取り出せました。

通信が終わっても、暗号文と長期秘密鍵があれば、秘密の材料を取り戻せる。まず、その入口を実験でも確認できました。

次に、同じ証明書でECDHE_RSAを使う

比較用サーバーを停止し、サーバーとクライアント双方の方式指定を、次のように変えます。

Bash
-tls1_2 \
-cipher ECDHE-RSA-AES256-GCM-SHA384 \
-groups X25519

これは先ほどのコマンドのオプションを置き換える部分です。server.crtとserver.keyは、そのまま使います。

今度のハンドシェイクには、ServerKeyExchangeが現れました。

>>> ClientHello
<<< ServerHello
<<< Certificate
<<< ServerKeyExchange
<<< ServerHelloDone
>>> ClientKeyExchange
>>> Finished
<<< Finished

ServerKeyExchangeで、サーバーの一時的な鍵交換の公開情報と、その情報に対する署名を送ります。クライアントもClientKeyExchangeで、自分の一時公開鍵を送ります。

今回のClientKeyExchangeで送られたX25519公開鍵は32バイトでした。この32バイトは、RSAで暗号化された共有秘密ではありません。双方が共有秘密を計算するための公開情報です。

接続結果には、Server Temp Key: X25519とPeer signature type: RSA-PSSが出ました。

RSA証明書を変えていないのに、RSAで材料を運ぶ処理から、X25519で材料を作ってRSAで署名する処理へ変わっています。

同じRSA証明書での実験RSA鍵輸送・TLS 1.2ECDHE_RSA・TLS 1.2TLS 1.3
証明書のRSA秘密鍵の仕事秘密の材料を復号するServerKeyExchangeに署名するCertificateVerifyに署名する
通信に使う秘密の作り方クライアントが作り、RSAで暗号化して渡す一時鍵の公開情報から双方で計算する今回はX25519の一時鍵で双方が計算する
鍵交換の公開情報・暗号文がある場所ClientKeyExchangeServerKeyExchangeとClientKeyExchangeClientHelloとServerHelloのkey_share
今回の通信の暗号AES-256-GCMAES-256-GCMAES-256-GCM

証明書の種類と、通信の暗号を揃えたまま、鍵交換の仕事だけを比べると、違いがはっきり見えました。

手元で再現するためのサーバー準備

ここからは、先ほどのOpenSSLログを再現するための準備です。

OpenSSL 3.xを使い、実験用のディレクトリで作業します。証明書の有効期間は1日です。今回作るCAは、OSの信頼済みストアへ登録しません。

まず、実験用CAを作ります。

Bash
mkdir tls-lab
cd tls-lab
umask 077

openssl req -x509 -newkey rsa:2048 -noenc \
  -keyout lab-ca.key \
  -out lab-ca.crt \
  -days 1 -sha256 \
  -subj "/CN=TLS Lab Root CA" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

次に、サーバーの秘密鍵とCSRを作ります。

Bash
openssl req -new -newkey rsa:2048 -noenc \
  -keyout server.key \
  -out server.csr \
  -subj "/CN=localhost"

サーバー証明書に入れる拡張を、server.extというファイルに保存します。

INI
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:localhost

CNだけに頼らず、SANへlocalhostを入れています。

digitalSignatureは署名の用途です。今回はTLS 1.2のRSA鍵輸送とも比較するため、鍵の暗号化に使えることを示すkeyEnciphermentも入れています。TLS 1.3のRSA署名だけを見る実験なら、後者は必要ありません。[3]

その内容を使い、実験用CAからサーバー証明書を発行します。

Bash
openssl x509 -req \
  -in server.csr \
  -CA lab-ca.crt \
  -CAkey lab-ca.key \
  -CAcreateserial \
  -out server.crt \
  -days 1 -sha256 \
  -extfile server.ext

最後に、TLSの検証用サーバーを起動します。使う方式を揃えるため、TLS 1.3、X25519、AES-256-GCMの暗号スイートを指定しています。

Bash
openssl s_server \
  -accept 127.0.0.1:18443 \
  -cert server.crt \
  -key server.key \
  -tls1_3 \
  -groups X25519 \
  -ciphersuites TLS_AES_256_GCM_SHA384 \
  -no_ticket \
  -www

-wwwで簡易HTTP応答を返すサーバーになります。別のターミナルから、「OpenSSLで、接続の順序を見てみる」のクライアント用コマンドで接続します。[6]

なお、TLS 1.3では-no_ticketだけではチケットの発行を止められません。今回のログにもNewSessionTicketが現れました。チケット自体を発行しない実験にする場合は、サーバーへ-num_tickets 0を指定します。[6]

公開サイトの様子を観察する場合は、接続先、SNI、検証する名前をそのサイトへ揃え、実験用の-CAfile指定を外します。

次回

次回は「暗号化と認証⑨ ― 共有秘密から鍵を作り、証明書と署名で相手を確かめる」を取り上げます。

今回のログを手掛かりに、共有秘密から用途別の鍵を作る理由と、証明書・署名で接続相手を確かめる仕組みを追います。

参考資料

  1. RFC 8446 — TLS 1.3:§2、§4.4、§6.1、§7。ハンドシェイク、CertificateVerify、Finished、鍵導出、接続の終了。
  2. OpenSSL 3.0 — openssl-s_client:メッセージ表示と検証オプション。
  3. RFC 5246 — TLS 1.2:旧来のRSA鍵交換と、そのほかの方式。
  4. RFC 10015 — Deprecating Obsolete Key Exchange Methods in TLS 1.2 and DTLS 1.2:2026年7月のRSA鍵輸送廃止と、その理由。
  5. OpenSSL 3.0 — openssl-ciphers:暗号スイートの標準名とOpenSSLの名前。
  6. OpenSSL 3.0 — openssl-s_server:検証用サーバーとチケットの設定。

返信を残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

CAPTCHA