[インデックス 12686] ファイルの概要
このコミットは、Go言語のcrypto/tls
パッケージにおけるTLSクライアントハンドシェイクの挙動を修正するものです。具体的には、サーバーからCertificateRequest
メッセージを受信した場合、クライアントが提供する証明書を持っていなくても、必ずCertificate
メッセージを送信するように変更します。これにより、TLSプロトコルの要件に準拠し、特定のTLS接続の問題(Go issue #3339)を解決します。
コミット
- コミットハッシュ:
aa1d4170a4f586bf2d9c68097f049977146bd31c
- 作者: Adam Langley agl@golang.org
- コミット日時: Mon Mar 19 12:34:35 2012 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/aa1d4170a4f586bf2d9c68097f049977146bd31c
元コミット内容
crypto/tls: always send a Certificate message if one was requested.
If a CertificateRequest is received we have to reply with a
Certificate message, even if we don't have a certificate to offer.
Fixes #3339.
R=golang-dev, r, ality
CC=golang-dev
https://golang.org/cl/5845067
変更の背景
この変更は、Go言語のcrypto/tls
パッケージが、TLSクライアントハンドシェイク中に特定のシナリオで正しく動作しないバグ(Go issue #3339: crypto/tls: client handshake regression
)を修正するために導入されました。報告された問題は、tls.Dial
がirc.freenode.net:6697
への接続に失敗するというものでした。
この問題の根本原因は、TLSプロトコルの仕様にありました。TLSハンドシェイクにおいて、サーバーがクライアント認証を要求するためにCertificateRequest
メッセージを送信した場合、クライアントはそれに対して必ずCertificate
メッセージで応答しなければなりません。たとえクライアントが提供できる証明書を持っていなかったとしても、空のCertificate
メッセージを送信する必要があります。
以前のcrypto/tls
の実装では、クライアントが提供する証明書を持っていない場合、CertificateRequest
を受信してもCertificate
メッセージを送信しないことがありました。このプロトコル違反が、一部のTLSサーバー(特に厳格な実装を持つサーバー)との接続確立を妨げ、ハンドシェイクの失敗につながっていました。このコミットは、このプロトコル違反を修正し、TLSの相互運用性を向上させることを目的としています。
前提知識の解説
TLSハンドシェイクの概要
TLS (Transport Layer Security) ハンドシェイクは、クライアントとサーバーが安全な通信チャネルを確立するために行う初期のネゴシエーションプロセスです。このプロセスには、プロトコルバージョンの合意、暗号スイートの選択、サーバー認証、オプションのクライアント認証、鍵交換、そしてセキュアなセッションの確立が含まれます。
クライアント認証 (Mutual TLS)
通常のTLSハンドシェイクでは、クライアントはサーバーの身元を検証しますが、サーバーはクライアントの身元を検証しません。しかし、より高いセキュリティが求められるシナリオでは、サーバーもクライアントの身元を検証することがあります。これを「クライアント認証」または「相互TLS (mTLS)」と呼びます。
クライアント認証のプロセスは、通常のハンドシェイクに以下のメッセージが追加されることで実現されます。
-
CertificateRequest
メッセージ:- 送信者: サーバー
- 目的: サーバーがクライアントに対してデジタル証明書の提示を要求するために送信します。これは、サーバーがクライアントの身元を検証したいという意思表示です。
- 内容: このメッセージには、サーバーが受け入れ可能な証明書の種類(例: RSA、ECDSA)や、サーバーが信頼する認証局(CA)の識別名(Distinguished Names: DNs)のリストが含まれることがあります。これにより、クライアントは適切な証明書を選択しやすくなります。
-
Certificate
メッセージ:- 送信者: クライアント(サーバーからの
CertificateRequest
に応答する場合) - 目的: クライアントが自身のデジタル証明書をサーバーに提示するために送信します。サーバーはこの証明書を検証し、クライアントの身元を確認します。
- 重要性: TLSプロトコルの仕様では、サーバーから
CertificateRequest
メッセージを受信した場合、クライアントは必ずCertificate
メッセージで応答しなければなりません。たとえクライアントが適切な証明書を所有していない場合でも、証明書リストが空のCertificate
メッセージを送信する必要があります。これは、サーバーがクライアント認証を要求したという事実に対するプロトコル上の応答であり、このメッセージを省略するとプロトコル違反となり、ハンドシェイクが失敗する可能性があります。
- 送信者: クライアント(サーバーからの
-
CertificateVerify
メッセージ:- 送信者: クライアント
- 目的: クライアントが自身の秘密鍵を所有していることを証明するために、ハンドシェイクのトランスクリプトのハッシュに署名したものを送信します。これは、提示された証明書が正当なものであることをサーバーに保証します。
RFC 4346 (TLS 1.1) の関連性
このコミットメッセージで言及されている「RFC 4346」は、TLS 1.1の仕様を定義する文書です。特にcertificateAuthorities
フィールドに関する記述は、CertificateRequest
メッセージがどのような情報を含むことができるかを示しています。このRFCは、TLSプロトコルの挙動を理解する上で重要な基盤となります。
技術的詳細
TLSハンドシェイクのclientHandshake
関数は、クライアントがサーバーとの接続を確立する際の主要なロジックを含んでいます。この関数内で、サーバーから受信したメッセージを解析し、それに応じて適切な応答を生成します。
問題となっていたのは、サーバーがCertificateRequest
メッセージ(certificateRequestMsg
型)を送信してきた場合のクライアントの挙動でした。
変更前のコードでは、certToSend
という変数がnil
(つまり、クライアントが送信する証明書を持っていない)の場合、Certificate
メッセージ(certMsg
)の生成と送信のブロック全体がスキップされていました。
// 変更前
if certToSend != nil { // certToSendがnilの場合、このブロックは実行されない
certMsg = new(certificateMsg)
certMsg.certificates = certToSend.Certificate
finishedHash.Write(certMsg.marshal())
c.writeRecord(recordTypeHandshake, certMsg.marshal())
}
しかし、TLSプロトコルの厳密な解釈では、CertificateRequest
を受信したという事実自体が、クライアントがCertificate
メッセージを送信するトリガーとなります。たとえクライアントが証明書を持っていなくても、空の証明書リストを持つCertificate
メッセージを送信することで、サーバーはクライアントが認証要求に応答したことを認識できます。この「空の証明書リスト」は、クライアントが認証を拒否した、または適切な証明書を所有していないことを意味しますが、プロトコル上は有効な応答です。
このコミットは、このプロトコル要件を満たすために、certRequested
という新しいブール変数を導入しました。CertificateRequest
メッセージが受信された場合、certRequested
がtrue
に設定されます。そして、Certificate
メッセージの送信ロジックは、certToSend != nil
ではなく、certRequested
がtrue
であるかどうかをチェックするように変更されました。
// 変更後
// If the server requested a certificate then we have to send a
// Certificate message, even if it's empty because we don't have a
// certificate to send.
if certRequested { // サーバーが証明書を要求した場合、必ずこのブロックが実行される
certMsg = new(certificateMsg)
if certToSend != nil { // 送信する証明書がある場合のみ、証明書リストを設定
certMsg.certificates = certToSend.Certificate
}
finishedHash.Write(certMsg.marshal())
c.writeRecord(recordTypeHandshake, certMsg.marshal())
}
この変更により、クライアントはCertificateRequest
を受信した際には常にCertificate
メッセージを送信するようになり、TLSプロトコルの仕様に準拠するようになりました。これにより、特定のTLSサーバーとの相互運用性の問題が解決され、Goのcrypto/tls
ライブラリの堅牢性が向上しました。
コアとなるコードの変更箇所
変更はsrc/pkg/crypto/tls/handshake_client.go
ファイルに集中しています。
--- a/src/pkg/crypto/tls/handshake_client.go
+++ b/src/pkg/crypto/tls/handshake_client.go
@@ -166,8 +166,11 @@ func (c *Conn) clientHandshake() error {
}
var certToSend *Certificate
+ var certRequested bool // 新しく追加された変数
certReq, ok := msg.(*certificateRequestMsg)
if ok {
+ certRequested = true // CertificateRequestを受信した場合にtrueに設定
+
// RFC 4346 on the certificateAuthorities field:
// A list of the distinguished names of acceptable certificate
// authorities. These distinguished names may specify a desired
@@ -238,9 +241,14 @@ func (c *Conn) clientHandshake() error {
}\n finishedHash.Write(shd.marshal())\n \n-\tif certToSend != nil { // 変更前: certToSendがnilでない場合のみ実行
+\t// If the server requested a certificate then we have to send a
+\t// Certificate message, even if it\'s empty because we don\'t have a
+\t// certificate to send.
+\tif certRequested { // 変更後: certRequestedがtrueの場合に実行
\t\tcertMsg = new(certificateMsg)\n-\t\tcertMsg.certificates = certToSend.Certificate // 変更前: 無条件に設定
+\t\tif certToSend != nil { // 変更後: certToSendがnilでない場合のみ設定
+\t\t\tcertMsg.certificates = certToSend.Certificate
+\t\t}
\t\tfinishedHash.Write(certMsg.marshal())\n \t\tc.writeRecord(recordTypeHandshake, certMsg.marshal())\n \t}
コアとなるコードの解説
-
var certRequested bool
の追加:clientHandshake
関数の冒頭に、certRequested
という新しいブール変数が宣言されました。この変数は、サーバーからCertificateRequest
メッセージが受信されたかどうかを追跡するために使用されます。 -
certRequested = true
の設定: サーバーから受信したメッセージmsg
がcertificateRequestMsg
型にキャスト可能(つまり、CertificateRequest
メッセージである)場合、certRequested
変数がtrue
に設定されます。これにより、クライアントが証明書要求を受け取ったという事実が記録されます。 -
Certificate
メッセージ送信ロジックの変更: 以前は、Certificate
メッセージを送信するかどうかの判断は、certToSend != nil
(クライアントが送信する証明書を持っているか)に依存していました。 変更後は、この条件がcertRequested
に置き換えられました。これにより、サーバーがCertificateRequest
を送信した場合、クライアントは常にCertificate
メッセージを送信するようになります。certMsg = new(certificateMsg)
: まず、新しいCertificate
メッセージ構造体が初期化されます。if certToSend != nil { certMsg.certificates = certToSend.Certificate }
: ここが重要な変更点です。クライアントが実際に送信する証明書(certToSend
)を持っている場合にのみ、その証明書がcertMsg.certificates
に設定されます。もしcertToSend
がnil
であれば、certMsg.certificates
は空のままになります。これにより、証明書がない場合でも空のCertificate
メッセージが送信されることが保証されます。finishedHash.Write(certMsg.marshal())
とc.writeRecord(recordTypeHandshake, certMsg.marshal())
: 生成されたCertificate
メッセージは、ハンドシェイクのハッシュに組み込まれ、その後、ネットワーク経由でサーバーに送信されます。
この修正により、GoのTLSクライアントは、TLSプロトコルの厳格な要件に準拠し、クライアント認証を要求するサーバーとの接続において、より堅牢で互換性のある挙動を示すようになりました。
関連リンク
- Go issue #3339: https://code.google.com/p/go/issues/detail?id=3339 (現在はGitHubに移行済み)
- Go CL 5845067: https://golang.org/cl/5845067
参考にした情報源リンク
- TLS handshake CertificateRequest Certificate message の解説:
- https://www.thesslstore.com/blog/what-is-a-tls-handshake/
- https://www.ibm.com/docs/en/ztpf/2020?topic=handshake-certificate-message
- https://medium.com/@anushasree.s/tls-handshake-explained-in-detail-with-diagrams-3e4222222222
- https://www.digicert.com/blog/what-is-a-tls-handshake
- https://www.cybersec.ee/blog/tls-handshake-explained/
- https://www.ibm.com/docs/en/ztpf/2020?topic=handshake-certificate-request-message
- https://www.entro.security/blog/what-is-a-tls-handshake