ここまでの記事では、API キーやアクセストークン、Webhook の署名など、API を使う相手を確かめるしくみを見てきました。どの記事でも前提にしていたのが「通信は TLS(HTTPS)で守られている」ことで、Bearer トークンの仕様(RFC 6750)も、トークンを送るときは必ず TLS を使うよう求めています。この記事からは、通信そのものを守るしくみに進みます。まずは TLS がハンドシェイクで何をしているのかを、RFC と、テスト用の環境で OpenSSL を動かした結果で確かめながら、最初の準備から一歩ずつ図にしました。
- TLS が守る3つのこと(見られない・変えられない・相手が本物)と、守れないこと
- 今使うバージョンは TLS 1.2 と 1.3。1.1 以前は使わない
- TLS 1.3 のハンドシェイクで、鍵を決めてサーバを確かめるまでの流れ
- クライアントがサーバ証明書で確かめること(つながり・名前・期限・持ち主)
- 0-RTT(early data)のしくみと、再送への対策
- OpenSSL と curl で、ハンドシェイクと証明書の確認、0-RTT を実際に見る方法
まずはたとえで:封をした書留と、窓口の身分証
大事な書類を送るとき、封筒に入れて封をし、割り印を押し、受け取る窓口では相手の身分証を確かめます。封をするのは中を見られないため、割り印は途中で中身を入れ替えられたら気づくため、身分証は渡す相手が本物かを確かめるためです。
TLS(Transport Layer Security)は、これをネットワークの上で行うしくみです。TLS 1.3 の仕様(RFC 8446)は、TLS が守るものとして、認証(サーバは必ず確かめる。クライアントを確かめるのは任意)、機密性(つながった後のデータは両端にしか見えない)、完全性(つながった後のデータは、気づかれずに書き換えることができない)の3つを挙げています。クライアントも証明書で確かめる使い方は、のちの mTLS の記事で扱います。
使う場面:API のトークンは TLS の中を通る
ブラウザで https:// のページを開くとき、アプリが API を呼ぶとき、どちらも HTTP を TLS の上で送っています(HTTPS)。API では、Authorization: Bearer … のトークンや API キーがヘッダに入って毎回流れるので、TLS がなければそのまま読まれてしまいます。RFC 6750 は、Bearer トークンを使うクライアントは必ず TLS(https)を使うこと、トークンを URL に入れないことを求めています。
一方で、TLS でも隠れないものがあります。IP アドレスやポートは、パケットを届けるために見えています。どのサーバ名につなぐかを伝える SNI(Server Name Indication)は、1つの IP アドレスで複数のサイトを動かすサーバのために、最初のメッセージ(ClientHello)に入れて送ります。TLS の推奨をまとめた RFC 9325 も、SNI から接続先のドメインが分かることを指摘しています。トークンを URL に入れないのは、URL がログや履歴に残りやすいからで、TLS で守られるかどうかとは別の理由です。
TLS の基本の形
TLS の位置と、中の2つ
TLS は、TCP と HTTP の間に入ります。HTTP から見ると、TCP の代わりに「暗号化された通り道」を使っているだけで、HTTP のリクエストやレスポンスの中身は変わりません。TLS の中は、大きく2つに分かれます。最初に1回だけ行い、相手を確かめて鍵を取り決める「ハンドシェイク」と、取り決めた鍵でデータを暗号化して運ぶ「レコード」です。
バージョン:今使うのは TLS 1.2 と 1.3
TLS には SSL の時代から続くいくつものバージョンがあります。TLS 1.0 と 1.1 は、今の推奨の暗号を使えないことなどから、RFC 8996(2021年)で正式に非推奨になり、使ってはいけないとされました。TLS を安全に使うための推奨をまとめた RFC 9325(2022年)は、TLS 1.2 には必ず対応し、TLS 1.3 にも対応して、使えるときは 1.3 を優先するよう求めています。
TLS 1.2 を使う場合は、暗号スイート(鍵の取り決め方・暗号・ハッシュの組み合わせ)の選び方も大事です。RFC 9325 は、前方秘匿性のある暗号スイートに対応して優先すること、鍵をサーバの RSA の公開鍵でそのまま送る方式(static RSA)を使わないことを求めています。前方秘匿性とは、あとでサーバの秘密鍵が漏れても、それより前に記録された通信は復号できない性質のことです。TLS 1.3 では static RSA の方式そのものがなくなり、公開鍵を使う鍵の取り決めはすべて前方秘匿性を持ちます。
誰が何を持つか
サーバは、自分の秘密鍵と、その公開鍵が入ったサーバ証明書を持ちます。サーバ証明書には、CA(認証局)が「この公開鍵は、この名前のサーバのもの」と署名しています。クライアントは、信頼する CA のルート証明書の一覧を持っていて、サーバ証明書がそこまでつながるかを確かめます。もう1つ大事なのが、接続のたびに作る「使い捨ての鍵ペア」です。データを暗号化する鍵は、この使い捨ての鍵から作るので、サーバの秘密鍵で直接暗号化するわけではありません。
全体の流れ
準備:誰を信頼するかと、自分を証明するものをそろえる
TLS で通信する前に、サーバには証明書と秘密鍵を、クライアントには信頼する CA を用意します。証明書の発行のしくみ(PKI)は次の記事で詳しく扱うので、ここでは流れだけを見ます。
| 手順 | 誰が・どこで | 何をするか | 鍵(物)はどうなるか・使う鍵 |
|---|---|---|---|
| ① | CA → クライアント | ルート証明書を配る | OS やブラウザに最初から入っている。社内の CA なら配布する |
| ② | 管理者・サーバで操作 | サーバの鍵ペアを作る | 秘密鍵はサーバの中にだけある |
| ③ | 管理者・サーバで操作 | CSR(証明書の申請書)を作る | 公開鍵と名前を入れ、秘密鍵で署名する |
| ④ | 管理者 → CA | CSR を CA に送る | 秘密鍵は送らない |
| ⑤ | CA | 名前を確かめて署名する | CA の秘密鍵で署名する |
| ⑥ | CA → 管理者 | サーバ証明書を渡す | 中間証明書も一緒に受け取る |
| ⑦ | 管理者・サーバで操作 | 証明書・秘密鍵・最低バージョンを設定する | サーバが秘密鍵と証明書を使える |
| ⑧ | サーバ | 待ち受ける | HTTPS は通常 443 番 |
ハンドシェイクとデータ(TLS 1.3)
準備ができたら、クライアントが API を呼ぶところから、レスポンスが返るまでを見ます。TLS 1.3 では、クライアントが最初のメッセージで鍵の材料(key_share)を送るので、サーバはすぐに鍵を決められます。ServerHello より後のハンドシェイクのメッセージは、すべて暗号化されます。
| 手順 | 誰が・どこで | 何をするか | 鍵(物)はどうなるか・使う鍵 |
|---|---|---|---|
| ⑨ | クライアント | 使い捨ての鍵ペアを作る | X25519 などの楕円曲線の鍵。接続ごとに作る |
| ⑩ | クライアント → サーバ | ClientHello を送る | 使い捨ての公開鍵(key_share)、使えるバージョンと暗号スイート、SNI、ALPN を入れる |
| ⑪ | サーバ | 鍵ペアを作り、共通の秘密を計算する | 自分の秘密鍵(使い捨て)と、クライアントの公開鍵から計算する |
| ⑫ | サーバ → クライアント | ServerHello を返す | サーバの使い捨ての公開鍵(key_share)と、選んだ暗号スイート |
| ⑬ | クライアント | 同じ共通の秘密を計算する | 秘密そのものはネットワークを流れていない。ここからハンドシェイク用の鍵で暗号化 |
| ⑭ | サーバ → クライアント | EncryptedExtensions を送る | ALPN の結果などを暗号化して送る |
| ⑮ | サーバ → クライアント | Certificate を送る | サーバ証明書と中間証明書(公開の情報) |
| ⑯ | サーバ → クライアント | CertificateVerify を送る | ここまでのやり取り全体に、サーバの秘密鍵で署名する |
| ⑰ | サーバ → クライアント | Finished を送る | ここまでのやり取り全体の MAC(改ざん検知の値) |
| ⑱ | クライアント | サーバを確かめる | 証明書を信頼する CA まで確かめ、⑯の署名を証明書の公開鍵で確かめる |
| ⑲ | クライアント → サーバ | Finished を送る | ここからアプリ用の鍵で暗号化 |
| ⑳ | クライアント → サーバ | HTTP リクエストを送る | トークンやデータはアプリ用の鍵で暗号化される |
| ㉑ | サーバ | 復号して処理する | トークンの検証は、TLS ではなく API が行う |
| ㉒ | サーバ → クライアント | HTTP レスポンスを返す | 同じくアプリ用の鍵で暗号化される |
⑨〜⑬は、Diffie-Hellman という方法で鍵を決めています。お互いに使い捨ての鍵ペアを作り、公開鍵だけを交換すると、両方が同じ「共通の秘密」を計算できます。途中で公開鍵を見た人には、この秘密は計算できません。データを暗号化する鍵は、この共通の秘密から作ります。
⑯の CertificateVerify が、サーバが本物であることの決め手です。証明書は公開の情報なので、だれでも手に入れて送ることができます。しかし、⑯の署名は、証明書の公開鍵と対になる秘密鍵を持っていないと作れません。しかも署名の対象は「このやり取り全体」なので、ほかの接続で作った署名を使い回すこともできません。
0-RTT(early data):前につないだ相手には、最初のメッセージでデータを送れる
TLS 1.3 には、一度つないだサーバにもう一度つなぐときの近道があります。1回目の接続の最後に、サーバは NewSessionTicket というチケットを送ります。クライアントはこのチケットと、次の接続で使う鍵(PSK:Pre-Shared Key、前もって共有した鍵)を保存しておきます。次の接続では ClientHello にチケットを入れ、サーバの返事を待たずに、最初のメッセージと一緒にデータを送れます。往復(RTT)を1回も待たずに送るので、0-RTT(early data)と呼びます。
| 項目 | 最初の図(図7) | この図(0-RTT) |
|---|---|---|
| データを送れる時期 | ハンドシェイクが終わってから(⑳) | 最初のメッセージと一緒(❹❺) |
| データを暗号化する鍵 | その接続の使い捨ての鍵から作る(⑪⑬) | チケットに結びついた PSK だけから作る(❺) |
| 前方秘匿性 | ある | ない。PSK が漏れると、記録された 0-RTT のデータを復号できる |
| 同じものを送り直されたら | サーバの使い捨ての鍵が毎回変わるので、通らない | 同じ鍵で復号できてしまう(❾)。接続をまたいだ再送の防止は保証されない |
RFC 8446 は、0-RTT のデータについて、前方秘匿性がないことと、接続をまたいだ再送の防止が保証されないことをはっきり書いています。たとえば「注文を作る」リクエストが 0-RTT で送られ、それがもう一度届くと、注文が2回作られるおそれがあります。そのため、0-RTT を受け付ける側で次のような対策をとります。
- チケットを1回限りにする:OpenSSL は、サーバで 0-RTT を有効にすると、既定でチケットを1回しか使えないようにします。同じチケットが2回目に届くと、普通のハンドシェイクに戻し、0-RTT のデータは無視します(図の❿)。この動きは、後の STEP 6 で実際に確かめます。
- 0-RTT で受けるのは安全なリクエストだけ:HTTP で 0-RTT を使うときの決まり(RFC 8470)は、クライアントは GET のような安全なメソッド(サーバの状態を変えないもの)だけを 0-RTT で送ってよく、安全でないメソッドや安全かどうか分からないメソッドは送ってはいけない、としています。
- サーバ側で待つか、断る:RFC 8470 は、サーバの対策として、0-RTT を TLS の段階で断る、ハンドシェイクが終わるまで処理を待つ、
425 Too Earlyを返して 0-RTT を使わずに送り直してもらう、の3つを挙げています。CDN などの中継の装置が 0-RTT で受けたリクエストを後ろに転送するときは、Early-Data: 1のヘッダを付けて知らせます。
API では、注文の作成や送金のように何度も実行されると困る処理を、0-RTT で受けないことが基本です。0-RTT を使うかどうかは、サーバや CDN の設定で決まるので、API を置く環境で有効になっているかを確かめておきます。
クライアントが証明書で確かめること
- つながり:サーバ証明書の署名を中間証明書で、中間証明書の署名をルート証明書で、とたどり、信頼する CA まで届くかを確かめます。
- 名前:接続先の名前が、証明書の SAN(Subject Alternative Name)の DNS 名にあるかを確かめます。RFC 9525 は、名前の確認に CN(コモンネーム)を使ってはいけないとしています。ワイルドカード(
*.example.com)は、いちばん左のラベル1つにだけ使えます。 - 期限:証明書の有効期間の中かを確かめます。
- 持ち主:CertificateVerify の署名を、証明書の公開鍵で確かめます。
TLS 1.2 との違い
TLS 1.2 も、正しく設定すれば今でも使えます。ただし、ハンドシェイクの形が違います。ECDHE を使う場合の流れは次のとおりです。
| 項目 | 最初の図(TLS 1.3) | この図(TLS 1.2) |
|---|---|---|
| データを送れるまで | 1往復(⑩⑫) | 2往復(❶〜❺、❻〜❽) |
| 鍵の材料を送る時期 | 最初の ClientHello に入れる | サーバの返事を待ってから送る(❹❻) |
| 証明書 | 暗号化されて届く(⑮) | 平文で流れる(❸) |
| サーバの署名 | やり取り全体に署名(⑯) | ECDHE の公開鍵などに署名(❹) |
| static RSA | ない | 設定によっては使えてしまうので、使わない設定にする |
| ChangeCipherSpec | 本来は不要 | 暗号化を始める合図(❼❽) |
試してみる(OpenSSL と curl)
ここからは、テスト用の環境で OpenSSL 3.0.13 と curl 8.5.0 を使い、自分で作った CA とサーバ証明書で TLS をつないだ結果を見ていきます。名前は api.example.test(試験用のドメイン)と localhost にしています。
テスト用の CA とサーバ証明書を作る(図6の①〜⑥)
まず、テスト用の CA の鍵と、自分で署名したルート証明書を作ります。これが図6の CA と、①でクライアントに配るルート証明書の代わりです。
テスト用の CA を作る(図6の①)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout ca.key -out ca.crt -days 30 -subj "/CN=Demo Root CA"
次に、サーバの鍵ペアと CSR を作ります。図6の②③にあたります。
サーバの鍵ペアと CSR を作る(図6の②③)
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout server.key -out server.csr -subj "/CN=api.example.test"
CA の鍵で CSR に署名し、サーバ証明書を作ります。名前は SAN(subjectAltName)に入れ、用途をサーバ認証(serverAuth)にします。図6の⑤⑥にあたります。
CA の鍵で署名してサーバ証明書を作る(図6の⑤⑥)
printf "subjectAltName=DNS:api.example.test,DNS:localhost\nextendedKeyUsage=serverAuth\n" > ext.cnf openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 7 -extfile ext.cnf
Certificate request self-signature ok subject=CN = api.example.test
できた証明書の中身を確かめます。発行者(issuer)がテスト用の CA で、SAN に2つの名前が入っています。
サーバ証明書の中身を見る
openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName,extendedKeyUsage
subject=CN = api.example.test issuer=CN = Demo Root CA notBefore=Nov 2 05:02:19 2026 GMT notAfter=Nov 9 05:02:19 2026 GMT X509v3 Subject Alternative Name: DNS:api.example.test, DNS:localhost X509v3 Extended Key Usage: TLS Web Server Authentication
サーバを起動する(図6の⑦⑧)
OpenSSL の s_server で、証明書と秘密鍵を使うテスト用のサーバを 8443 番で起動します。-www を付けると、HTTP のリクエストに簡単なページを返します。
テスト用の TLS サーバを起動する(サーバで操作、図6の⑦⑧)
openssl s_server -accept 8443 -cert server.crt -key server.key -www
大事なのは -key で渡す秘密鍵の使われ方です。サーバはこの鍵で CertificateVerify の署名(図7の⑯)を作るだけで、鍵そのものはネットワークに出ません。
TLS 1.3 でつないで、結果を見る(図7の⑨〜⑲)
OpenSSL の s_client でつなぎます。-CAfile で信頼する CA を、-verify_hostname で確かめる名前を指定し、-brief で結果だけを表示します。
s_client で接続する
echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -verify_hostname api.example.test -brief
CONNECTION ESTABLISHED Protocol version: TLSv1.3 Ciphersuite: TLS_AES_256_GCM_SHA384 Peer certificate: CN = api.example.test Hash used: SHA256 Signature type: ECDSA Verification: OK Verified peername: api.example.test Server Temp Key: X25519, 253 bits DONE
暗号スイートの TLS_AES_256_GCM_SHA384 は、データを AES-256-GCM で暗号化し、鍵を作るのに SHA-384 を使う、という意味です。Signature type: ECDSA は、サーバの鍵が楕円曲線の鍵なので、CertificateVerify の署名も ECDSA になっていることを示します。-CAfile と -verify_hostname を付けないと、確かめる相手や名前がないまま結果が出るので、確認のつもりで使うときは必ず付けます。
ハンドシェイクのメッセージを見る(図7の⑩〜⑲、図10)
-trace を付けると、やり取りしたメッセージが表示されます。量が多いので、レコードの種類とメッセージ名の行だけを取り出しました。
TLS 1.3 のメッセージを見る(図7の⑩〜⑲)
echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -trace 2>&1 | grep -E "^(Sent|Received) Record|Content Type|^ [A-Za-z]+, Length"
Sent Record
Content Type = Handshake (22)
ClientHello, Length=309
Received Record
Content Type = Handshake (22)
ServerHello, Length=118
Received Record
Content Type = ChangeCipherSpec (20)
Received Record
Content Type = ApplicationData (23)
Inner Content Type = Handshake (22)
EncryptedExtensions, Length=2
Received Record
Content Type = ApplicationData (23)
Inner Content Type = Handshake (22)
Certificate, Length=448
Received Record
Content Type = ApplicationData (23)
Inner Content Type = Handshake (22)
CertificateVerify, Length=75
Received Record
Content Type = ApplicationData (23)
Inner Content Type = Handshake (22)
Finished, Length=48
Sent Record
Content Type = ChangeCipherSpec (20)
Sent Record
Content Type = ApplicationData (23)
Inner Content Type = Handshake (22)
Finished, Length=48
Sent Record
Content Type = ApplicationData (23)
Inner Content Type = Alert (21)
ServerHello までは Content Type = Handshake (22) で、平文のハンドシェイクです。EncryptedExtensions からは外側が ApplicationData (23) になり、暗号化された中身(Inner Content Type)がハンドシェイクだと分かります。図7で暗号化の帯に入れた⑭〜⑲が、ここにあたります。
途中に出てくる ChangeCipherSpec は、TLS 1.3 では鍵の切り替えには使いません。OpenSSL の説明では、TLS 1.3 を TLS 1.2 のように見せて、TLS 1.3 を知らない中継機器に接続を切られないようにするためのダミーで、既定で送ります。受け取った側も無視します。
同じことを TLS 1.2 で行うと、次のようになります。
TLS 1.2 のメッセージを見る(図10の❶〜❽)
echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -tls1_2 -trace 2>&1 | grep -E "^(Sent|Received) Record|Content Type|^ [A-Za-z]+, Length"
Sent Record
Content Type = Handshake (22)
ClientHello, Length=204
Received Record
Content Type = Handshake (22)
ServerHello, Length=61
Received Record
Content Type = Handshake (22)
Certificate, Length=445
Received Record
Content Type = Handshake (22)
ServerKeyExchange, Length=111
Received Record
Content Type = Handshake (22)
ServerHelloDone, Length=0
Sent Record
Content Type = Handshake (22)
ClientKeyExchange, Length=33
Sent Record
Content Type = ChangeCipherSpec (20)
Sent Record
Content Type = Handshake (22)
Finished, Length=12
Received Record
Content Type = Handshake (22)
NewSessionTicket, Length=182
Received Record
Content Type = ChangeCipherSpec (20)
Received Record
Content Type = Handshake (22)
Finished, Length=12
Sent Record
Content Type = Alert (21)
こちらは Certificate も Handshake (22) のまま、つまり平文で流れています。ServerKeyExchange と ClientKeyExchange で ECDHE の公開鍵をやり取りし、ChangeCipherSpec の後から暗号化されます。サーバからの NewSessionTicket は、次に接続するときの再開に使う情報です。
2つを並べると、TLS 1.3 では ServerHello の直後から鍵のマーク(暗号化)が付き、TLS 1.2 では Finished まで付かないことが分かります。TLS 1.2 の Finished は、外側の種類は Handshake のままですが、ChangeCipherSpec の後なので暗号化されています。
curl で HTTPS のリクエストを送る(図7の⑳〜㉒)
最後に、API のクライアントと同じように curl でリクエストを送ります。--cacert で信頼する CA を指定し、-v の表示から TLS と証明書の行を取り出しました。
curl で HTTPS のリクエストを送る
curl -sv -o /dev/null --cacert ca.crt https://localhost:8443/ 2>&1 | grep -E "ALPN|TLSv1|SSL|subject|issuer|date|> GET|< HTTP"
* ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * subject: CN=api.example.test * start date: Nov 2 05:02:19 2026 GMT * expire date: Nov 9 05:02:19 2026 GMT * subjectAltName: host "localhost" matched cert's "localhost" * issuer: CN=Demo Root CA * SSL certificate verify ok. > GET / HTTP/1.1 * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * old SSL session ID is stale, removing < HTTP/1.0 200 ok * TLSv1.3 (IN), TLS alert, close notify (256): * TLSv1.3 (OUT), TLS alert, close notify (256):
ALPN: curl offers h2,http/1.1 は、TLS の中で使うアプリのプロトコル(HTTP/2 か HTTP/1.1 か)を、ハンドシェイクの中で決める ALPN の提案です。このテストのサーバは ALPN に対応していないので、決まらずに既定(HTTP/1.1)で送っています。証明書の名前(localhost)が合い、確認が通った後に、GET のリクエストが送られています。このリクエストに Authorization ヘッダを付けても、ネットワーク上では暗号化されて見えません。
0-RTT を試す(図8の❶〜❿)
最後に、0-RTT の動きを確かめます。s_server を -early_data 付きで起動し直し、同じクライアントから3回つなぎます。-naccept 3 は3回つないだら終わる指定、前の sleep 90 | は s_server の標準入力を開いたままにするためのものです。
0-RTT を受け付けるサーバを起動する(サーバで操作)
sleep 90 | openssl s_server -accept 8443 -cert server.crt -key server.key -early_data -naccept 3
0-RTT で送るデータとして、HTTP のリクエストを1つファイルに書いておきます。
0-RTT で送るデータを用意する
printf 'GET /api/orders HTTP/1.1\r\nHost: api.example.test\r\n\r\n' > early.txt
1回目は普通につなぎ、届いたチケットを -sess_out でファイルに保存します。(sleep 1; echo Q) は、チケットが届くまで少し待ってから切るためのものです。
1回目:普通につないでチケットを保存する(図8の❶〜❸)
(sleep 1; echo Q) | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -sess_out sess.pem 2>&1 | grep -E "^(New|Reused), |^Early data"
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Early data was not sent
2回目は -sess_in でチケットを使い、-early_data で用意したデータを 0-RTT で送ります。
2回目:チケットを使って 0-RTT で送る(図8の❹〜❽)
(sleep 1; echo Q) | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -sess_in sess.pem -early_data early.txt 2>&1 | grep -E "^(New|Reused), |^Early data"
Reused, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Early data was accepted
3回目は、2回目とまったく同じコマンドで、同じチケットと同じデータをもう一度送ります。
3回目:同じチケットでもう一度送る(図8の❾❿)
(sleep 1; echo Q) | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -sess_in sess.pem -early_data early.txt 2>&1 | grep -E "^(New|Reused), |^Early data"
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Early data was rejected
2回目は Reused(チケットで再開)になり、0-RTT のデータが受け入れられました。3回目は、同じチケットがもう使えないので New(普通のハンドシェイク)に戻り、0-RTT のデータは断られました。これが OpenSSL の既定の再送対策です。なお、sess.pem には再開に使う秘密の情報が入るので、試したあとは削除しておきます。
ここからは Microsoft の場合です
ここからは、Microsoft のサービスで TLS がどう扱われているかを見ます。API を呼ぶクライアントにとっては Entra ID(トークンをもらう先)、API を置く側にとっては Azure App Service が身近な例です。
- Entra ID:2022年1月31日から、Entra ID に接続するクライアントは TLS 1.2 以上が必須になりました。TLS 1.0・1.1 と 3DES の暗号スイートは使えません。古い .NET Framework のアプリでは、OpenID Connect の設定を取れずにサインインに失敗し、
AADSTS1002016などのエラーになります。 - App Service:受け付ける最低の TLS バージョン(Minimum Inbound TLS Version)を設定できます。新しいアプリの既定は TLS 1.2 で、1.3 も選べます。デプロイなどに使う SCM(Kudu)サイトには別の設定があり、デプロイスロットごとにも設定します。
- .NET のクライアント:Microsoft は、TLS のバージョンをコードに書かず、OS に選ばせることを勧めています。書くと、新しいバージョンが使えるようになっても使われません。
SslProtocols.Defaultは SSL 3.0 と TLS 1.0 になるので使いません。
App Service の最低 TLS バージョンを 1.2 にする(Azure CLI)
az webapp config set --name <app-name> --resource-group <resource-group> \
--min-tls-version 1.2
App Service の最低 TLS バージョンを確かめる(Azure CLI)
az webapp show --name <app-name> --resource-group <resource-group> \
--query "siteConfig.{siteTls:minTlsVersion, scmTls:scmMinTlsVersion}" \
--output table
Microsoft は、App Service・Azure Functions・Logic Apps(Standard)で、2027年5月31日に受け付ける TLS 1.0・1.1 を廃止するとしています。その日以降は、アプリの設定にかかわらず、TLS 1.0・1.1 でつなぐクライアントは接続できなくなります。古い .NET Framework、古い Java、IoT 機器、Webhook を送ってくる外部サービスなどが、TLS 1.2 以上で接続できるかを確かめておきます。
よくある間違い
| 間違い | どうするか |
|---|---|
| エラーが出たので、証明書の確認を止める | 確認を止めると、相手が本物かを確かめないまま、トークンを送ってしまう。原因(CA・名前・期限)を直す |
| サーバ証明書だけを設定し、中間証明書を送らない | クライアントがルートまでたどれない。中間証明書も一緒に設定する |
| IP アドレスでつなぐ | 証明書の SAN に IP がなければ名前が合わない。証明書に入っている名前でつなぐ |
| クライアントのコードに TLS 1.2 を決め打ちする | 1.3 が使えなくなる。OS やライブラリの既定に任せる |
| TLS で守られているから、トークンを URL に入れてもよいと考える | URL はログや履歴に残る。トークンはヘッダで送る |
| TLS があれば、API の認証はいらないと考える | TLS が確かめるのはサーバ。クライアントや利用者を確かめるのは、トークンや mTLS の仕事 |
うまくいかないとき
TLS のエラーは、ハンドシェイクのどの確認で止まったかが分かると、原因を絞れます。
STEP 1〜2 のテスト用サーバで、よくあるエラーをわざと起こしてみました。まず、信頼する CA を指定せずに curl でつないだ場合です。
信頼する CA がない(curl)
curl -sS -o /dev/null https://localhost:8443/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
次に、証明書の SAN にない名前(IP アドレス)でつないだ場合です。
名前が証明書と合わない(curl)
curl -sS -o /dev/null --cacert ca.crt https://127.0.0.1:8443/
curl: (60) SSL: no alternative certificate subject name matches target host name '127.0.0.1'
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
OpenSSL の s_client は調べるための道具なので、証明書の確認に失敗しても、そのまま接続を続けます。結果の Verification error を見落とさないようにします。
信頼する CA がない(s_client。確認に失敗しても接続が続く)
echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -brief
depth=0 CN = api.example.test verify error:num=20:unable to get local issuer certificate depth=0 CN = api.example.test verify error:num=21:unable to verify the first certificate CONNECTION ESTABLISHED Protocol version: TLSv1.3 Ciphersuite: TLS_AES_256_GCM_SHA384 Peer certificate: CN = api.example.test Hash used: SHA256 Signature type: ECDSA Verification error: unable to verify the first certificate Server Temp Key: X25519, 253 bits DONE
-verify_return_error を付けると、確認に失敗した時点でハンドシェイクを止めます。アプリや curl の動きに近くなります。
確認に失敗したら止める(s_client)
echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -brief -verify_return_error
depth=0 CN = api.example.test
verify error:num=20:unable to get local issuer certificate
40770A761D7F0000:error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:../ssl/statem/statem_clnt.c:1889:
最後に、TLS 1.1 でつなごうとした場合です。このテストで使った OpenSSL 3.0 の既定の設定では、使えるプロトコルがないとして、接続を始めませんでした。
TLS 1.1 でつなごうとする
echo Q | openssl s_client -connect localhost:8443 -tls1_1 -brief
4057C822447F0000:error:0A0000BF:SSL routines:tls_setup_handshake:no protocols available:../ssl/statem/statem_lib.c:104:
| エラー・症状 | 意味 | 確かめること |
|---|---|---|
| unable to get local issuer certificate | 信頼する CA までたどれない | サーバが中間証明書を送っているか。社内の CA なら、クライアントにルート証明書を入れたか |
| no alternative certificate subject name matches | 接続先の名前が SAN にない | 証明書の SAN と、つなぐときの名前(IP ではなく DNS 名か) |
| 証明書の期限切れのエラー | 有効期間の外 | 証明書の notAfter と、クライアントの時刻 |
| no protocols available | クライアント側で使えるバージョンがない | 指定したバージョンと、OpenSSL などの既定の設定 |
| サーバの最低バージョンを上げたら、古いクライアントがつながらない | クライアントが TLS 1.2 以上に対応していない | クライアントの OS・ランタイム(.NET Framework 4.7 未満など)を新しくする |
| AADSTS1002016 | Entra ID に TLS 1.0・1.1 か 3DES でつないだ | .NET Framework を 4.7 以上にする。バージョンの決め打ちを外す |
企業での例
TLS を復号する装置があるとき
企業のネットワークでは、ファイアウォールやプロキシで TLS を復号し、中身を検査していることがあります。たとえば Palo Alto Networks の次世代ファイアウォールの SSL Forward Proxy では、ファイアウォールがクライアントとファイアウォール、ファイアウォールとサーバの2つのセッションを作り、サーバ証明書のコピーをクライアントに送ります。
このとき PC が確かめるのは、ファイアウォールが作ったコピーの証明書です。PC が社内の CA を信頼していないと、「うまくいかないとき」で見た「信頼する CA までたどれない」エラーになります。また、ファイアウォールの中ではトークンも平文で見えるので、復号したデータのログの扱いや、装置の管理者の権限も TLS と同じくらい大事になります。
| 場面 | やること | 押さえどころ |
|---|---|---|
| 社内の API を HTTPS にする | サーバ証明書と中間証明書を設定し、最低バージョンを 1.2 にする | 名前は SAN に入れる。証明書の期限を管理する |
| 古い業務システムが TLS 1.0 でしかつながらない | 新しいバージョンのソフトや OS に入れ替える計画を立てる | TLS 1.0・1.1 は使ってはいけない(RFC 8996)。クラウド側から先に止まる |
| Azure App Service に API を置く | 最低 TLS バージョンを 1.2(または 1.3)にする | SCM サイトとスロットも設定する |
| 社内で TLS を復号して検査する | PC に社内の CA のルート証明書を配る | 復号したデータの扱いと、装置の管理を厳しくする |
まとめ
| 項目 | 内容 |
|---|---|
| 守るもの | 機密性(見られない)・完全性(変えられない)・認証(サーバは必ず、クライアントは任意) |
| 守れないもの | IP アドレス・ポート・接続先の名前(SNI)・通信の量や時刻 |
| バージョン | TLS 1.2 と 1.3 を使う。1.3 を優先。1.1 以前は使わない |
| TLS 1.3 の流れ | ClientHello と ServerHello で鍵を決め、その後は暗号化。証明書と署名でサーバを確かめる |
| 0-RTT | 前につないだ相手に、最初のメッセージでデータを送れる。前方秘匿性がなく、再送を防げないので、安全なリクエストだけにする |
| 証明書の確認 | 信頼する CA までのつながり・名前(SAN)・期限・持ち主(署名) |
| Microsoft の場合 | Entra ID は TLS 1.2 以上が必須。App Service は最低バージョンを設定する。.NET はバージョンを決め打ちしない |
参考にした資料
この記事は、次の標準と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3(IETF):TLS が守る3つ、TLS 1.2 からの主な変更(static RSA の廃止、ServerHello 後の暗号化)、鍵の取り決め方、メッセージの流れ、CertificateVerify と Finished、NewSessionTicket と PSK による再開、0-RTT のデータに前方秘匿性がなく再送の防止が保証されないこと
- RFC 8470: Using Early Data in HTTP(IETF):0-RTT で送ってよいのは安全なメソッドだけであること、サーバの3つの対策、425 Too Early、Early-Data ヘッダ
- RFC 5246: The Transport Layer Security (TLS) Protocol Version 1.2(IETF):TLS 1.2 のハンドシェイクの流れ、発行年
- RFC 8996: Deprecating TLS 1.0 and TLS 1.1(IETF):TLS 1.0・1.1 を使ってはいけないこと
- RFC 9325: Recommendations for Secure Use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS)(IETF):使ってよいバージョン、前方秘匿性と static RSA、推奨の暗号スイート、SNI で接続先が分かること、ALPN、HSTS
- RFC 9525: Service Identity in TLS(IETF):名前の確認に SAN の DNS 名を使い、CN を使わないこと、ワイルドカードの決まり
- RFC 6066: Transport Layer Security (TLS) Extensions: Extension Definitions・RFC 7301: Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension(IETF):SNI の目的、ALPN でアプリのプロトコルを決めること
- RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage(IETF):Bearer トークンは必ず TLS で送ること、URL に入れないこと
- SSL_CTX_set_options(OpenSSL 3.0 のドキュメント)・SSL_read_early_data(OpenSSL 3.0 のドキュメント)(OpenSSL):TLS 1.3 でダミーの ChangeCipherSpec を送る設定とその理由、0-RTT を有効にすると既定でチケットが1回限りになる再送対策
- Enable TLS 1.2 support in your environment for Microsoft Entra・Microsoft Entra applications using TLS 1.0/1.1 fail to authenticate(Microsoft Learn):Entra ID が TLS 1.0・1.1 と 3DES を止めた時期、影響を受けるもの、AADSTS1002016
- What is TLS/SSL in Azure App Service?・Manage minimum TLS versions for Azure App Service, Azure Functions, and Logic Apps (Standard)(Microsoft Learn):最低 TLS バージョンの既定と設定、SCM サイトとスロット、2027年5月31日の TLS 1.0・1.1 の廃止、古い TLS を使いがちなクライアント
- Transport Layer Security (TLS) best practices with .NET Framework(Microsoft Learn):TLS のバージョンを決め打ちせず OS に任せること、SslProtocols.Default を使わないこと
- SSL Forward Proxy(Palo Alto Networks):ファイアウォールが2つのセッションを作り、サーバ証明書のコピーを送ること
あとがき
ハンドシェイクを1つずつ追ってみて、TLS 1.3 では ServerHello の直後から暗号化が始まり、証明書のやり取りまで暗号の中で行われていることが、-trace の表示ではっきり見えたのが印象に残りました。また、「証明書は公開の情報で、本物の証拠は秘密鍵での署名」という点は、これまでの記事の JWT の署名とも同じ考え方です。次の記事では、その証明書を発行して信頼をつなぐしくみ(PKI)を扱います。
コメント