Dev Study
← HTTPとTLSの仕組み
レッスン 6 / 12·約 9 分·第18期 HTTPとTLSの仕組み

署名しているのは証明書ではなく、この会話

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力今ここ
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口今ここ
  6. アプリ処理
  7. DB保存

前回は、相手の選ぶ群を推測して1往復を削ることを見ました。今回、両端が共有しているのは、その接続で交わしたメッセージの記録そのものです。

まず結論

TLS 1.3 のサーバは、証明書を送った直後に CertificateVerify を送ります。署名しているのは証明書ではなく、その接続でここまでに交わしたメッセージ全体のハッシュ(トランスクリプトハッシュ)です。

両端が同じ会話の記録を持っているので、署名の対象を送り直さなくても、クライアントは自分で同じハッシュを計算して検証できます。証明書に付いた署名は何度つないでも同じ値ですが、CertificateVerify の署名は接続ごとに変わります。

? 読む前に予測

同じ Ed25519 の鍵と証明書を使うサーバへ、2回つなぎます。受け取った証明書の署名値と、CertificateVerify の署名値は、1回目と2回目でどうなるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

証明書は誰でも送れる。送れないのは、この会話への署名

先に2つの語を決めておきます。署名は秘密鍵で作り、対応する公開鍵で誰でも検証できる値です。MAC は両端が共有する鍵で作り、受け手が同じ鍵で計算し直して照合する値です。作れるのが「秘密鍵を持つ側だけ」か「共有鍵を持つ両方」か、が違います。

TLS 1.3 は RFC 9846(2026年7月。TLS 1.3 を定めていた RFC 8446 を廃止した改訂版)で定められています。以下の逐語は、断りがなければ 8446 と同文です。

サーバはハンドシェイクの途中で証明書を送ります。ところが証明書は公開されたデータで、同じバイト列が誰にでも配られます。証明書を送れたことは、何の証明にもなりません。 手に入れた証明書を送り直すだけなら、秘密鍵を持たない第三者にもできます。

そこで TLS 1.3 は、証明書の直後にもう1つメッセージを置きます。

CertificateVerify: A signature over the entire handshake using the private key corresponding to the public key in the Certificate message.

署名の対象は ハンドシェイク全体です。§4.5 はもっと正確に A signature over the value Transcript-Hash(Handshake Context, Certificate). と書き、§4.1 がその値を定義します。

This value is computed by hashing the concatenation of each included handshake message, including the handshake message header carrying the handshake message type and length fields, but not including record layer headers.

ClientHello にはクライアントが接続ごとに作る32バイトの乱数が入るので、この会話は前もって存在しません。その記録に署名できるのは、今この場で秘密鍵を持っている者だけです。

もう1つ大事な点があります。CertificateVerify の中身は、署名方式と署名値だけです。署名した対象そのものは送りません。 クライアントも同じメッセージを送受信してきたので、同じハッシュを自分で計算できるからです。共有しているものは送らずに済む、というこのコースの見方がそのまま当てはまります。

CertificateVerify が署名する範囲(証明書で認証するサーバの場合)
ClientHello接続ごとの乱数を含む
ServerHello
EncryptedExtensions
Certificate誰にでも同じバイト列
両端がそれぞれ連結してハッシュする
トランスクリプトハッシュ送られない。両端が別々に計算する
サーバが秘密鍵で署名する
CertificateVerify署名方式と署名値だけを送る
  • ・実際の署名対象は、0x20 を64個・文脈文字列・区切りの 0 を前に付けた列(RFC 9846 §4.5.2)
  • ・クライアントは証明書の公開鍵で検証し、失敗すれば decrypt_error で切る
具体実物

同じ鍵で2回つなぐ

手元で確かめます(macOS、OpenSSL 3.6.3、127.0.0.1)。Ed25519 の鍵と証明書を作ってサーバを立て、s_client -trace で2回つなぎました。

echo | openssl s_client -connect 127.0.0.1:18448 -trace を2回(該当行の抜粋。16進は途中を…で省略)追跡ログ
# 1回目
Signature Value:
f1:5f:89:57:72:fa:38:6a:…:24:0b
CertificateVerify, Length=68
Signature (len=64): FCAE605312AFFBA9…6131890D
# 2回目
Signature Value:
f1:5f:89:57:72:fa:38:6a:…:24:0b
CertificateVerify, Length=68
Signature (len=64): 71F527FEAEADAE99…D510A82605

Signature Value は証明書に付いている署名で、2回とも全桁が同じでした。CertificateVerify の署名は違います。

ただし「値が違う」だけでは証拠が足りません。署名方式によっては、同じ内容でも署名のたびに値が変わるからです。そこで同じファイルに2回署名しました。

同じメッセージ(same message)に、各コマンドを2回ずつ実行して署名する(tr で改行を消しているので、出力は実行ごとに1行へ分けて表示。16進は途中を…で省略)コマンド出力
$ openssl pkeyutl -sign -inkey l6-ed-key.pem -rawin -in l6-msg.bin | xxd -p | tr -d '\n'
bc76e5cac5eeccca…e3b31f04
$ openssl pkeyutl -sign -inkey l6-ed-key.pem -rawin -in l6-msg.bin | xxd -p | tr -d '\n'
bc76e5cac5eeccca…e3b31f04
$ openssl dgst -sha256 -sign l5-key.pem l6-msg.bin | xxd -p | tr -d '\n'
30460221009fa118…daae994d
$ openssl dgst -sha256 -sign l5-key.pem l6-msg.bin | xxd -p | tr -d '\n'
304502200ca9fd69…6fcd9d0b2c

Ed25519 は2回とも同じ値、OpenSSL 既定の ECDSA(P-256)は毎回違う値でした。Ed25519 のサーバで CertificateVerify が変わったのは、署名した内容、つまり会話が変わったからです。

✓ ここまでの確認

同じ Ed25519 の鍵を使うサーバで、CertificateVerify の署名値が2回の接続で違っていました。これが示すことはどれですか。

抽象原理

署名と MAC は、同じ記録を別の鍵で閉じる

CertificateVerify の次に Finished が来ます。§4.5 は A MAC over the value Transcript-Hash(Handshake Context, Certificate, CertificateVerify) using a MAC key derived from the Base Key. と書きます。入力は同じトランスクリプト(今度は CertificateVerify まで)で、違うのは鍵です。

鍵作れる者示すもの
CertificateVerify証明書の秘密鍵サーバだけ秘密鍵を今持っている
Finished鍵交換から導いた鍵両端同じ鍵を導けた・記録が一致した

Finished の鍵は鍵交換から導いたものです。だから Finished が正しくても、証明書の秘密鍵を持っている証明にはなりません。 役割が分かれているので、証明書で認証する側は CertificateVerify を省けません(Servers MUST send this message when authenticating via a certificate.)。

署名の前に付ける文脈文字列も同じ発想です。

It is used to provide separation between signatures made in different contexts, helping against potential cross-protocol attacks.

署名は「何に署名したか」でしか意味を持ちません。だから対象に、この接続の記録と、この用途の名前の両方を含めています。

具体実物

verify_data も接続ごとに変わる

Finished の値 verify_data は HMAC です。値は接続ごとに変わりました(先頭8桁)。ただし、これは Ed25519 の実演とは違い、会話が変わったことだけの証拠にはなりません。HMAC の鍵 finished_key も接続ごとの鍵交換(Base Key)から導かれるので、鍵と会話の両方が接続ごとに変わるからです。

OpenSSL 3.6.3 同士、サーバ Finished / クライアント Finished の verify_data(先頭8桁)表
条件 長さ 1回目 2回目
既定(TLS_AES_256_GCM_SHA384) 48 59D023AA / 2556FAA4 725861BB / 6EBB1EE7
TLS_AES_128_GCM_SHA256 32 489B9198 / 7026D15A 31164D94 / B762E180
Ed25519 の証明書(同上の既定) 48 50B4946A / 1CBA3657 549F2B6F / 377C9903

長さはスイートで変わりました。SHA-384 のスイートで48バイト、SHA-256 のスイートで32バイトです。表に無い TLS_CHACHA20_POLY1305_SHA256 でも32バイトでした(検証時と監査時の実測)。RFC は In previous versions of TLS, the verify_data was always 12 octets long. In TLS 1.3, it is the size of the HMAC output for the Hash used for the handshake. と書いています。「verify_data は48バイト」と覚えると、スイートが変わった瞬間に外れます。

✓ ここまでの確認

サーバの Finished を受け取り、verify_data の検証に成功しました。これでサーバが証明書の秘密鍵を持っていると言えますか。

⇄ 2つを見比べる
具体例 A

証明書に付いた署名

証明書を作ったときに一度だけ作られ、証明書と一緒に配られる値です。実測でも2回の接続で全桁が同じでした。持ち主以外でもそのまま送り直せるので、今つないでいる相手が秘密鍵を持つことは示しません。

受け取った証明書(1回目と2回目で同一。該当行の抜粋)追跡ログ
Issuer: CN = localhost
Subject: CN = localhost
Signature Algorithm: ED25519
Signature Value:
f1:5f:89:57:72:fa:38:6a:…:24:0b
具体例 B

CertificateVerify の署名

その接続のトランスクリプトハッシュに、接続のたびに作られます。対象に接続ごとの乱数が入るので、前もって作っておくことも、他の接続から持ってくることもできません。

CertificateVerify(1回目 / 2回目)追跡ログ
Signature Algorithm: ed25519 (0x0807)
Signature (len=64): FCAE605312AFFBA9…6131890D
Signature Algorithm: ed25519 (0x0807)
Signature (len=64): 71F527FEAEADAE99…D510A82605

同じ鍵方式の署名なのに、片方は「持ち主である」ことを示せず、もう片方は示せます。何が違うのでしょうか。

署名の対象が、いつ存在するようになったかを比べてください。

この章のまとめ

  • 証明書は誰にでも同じバイト列なので、送れたことは証明にならない。
  • CertificateVerify は トランスクリプトハッシュへの署名。対象は送らず、両端が同じ記録から別々に計算する。
  • Ed25519 は同じ内容なら同じ署名。それでも接続ごとに値が変わった → 会話が変わった。OpenSSL 既定の ECDSA の値の違いは証拠にならない。
  • Finished は同じ記録への MAC。鍵は鍵交換由来なので、秘密鍵の所有は示さない。
  • verify_data の長さは スイートのハッシュ長(SHA-256 系32、SHA-384 は48)。TLS 1.2 以前は12固定。
  • Verify return code は 連鎖の検証で、CertificateVerify の検証とは別。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

TLS 1.3 の CertificateVerify で、サーバが署名する内容はどれですか。

用語集(5語)
署名
秘密鍵で作り、対応する公開鍵で誰でも検証できる値。作れるのは秘密鍵を持つ側だけ。
MAC
両端が共有する鍵で作り、受け手が同じ鍵で計算し直して照合する値。鍵を持つ側ならどちらでも作れる。TLS 1.3 の Finished は HMAC を使う。
トランスクリプトハッシュ
その接続で交わしたハンドシェイクメッセージを連結してハッシュした値。両端が同じメッセージを持つので、別々に計算しても同じになる。
CertificateVerify
トランスクリプトハッシュへの署名。証明書に対応する秘密鍵を持っていることを示す。
Finished / verify_data
トランスクリプトハッシュへの HMAC。長さはスイートのハッシュ長で、SHA-256 系は32バイト、SHA-384 は48バイト。