Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.Dialirc.freenode.net:6697への接続に失敗するというものでした。

この問題の根本原因は、TLSプロトコルの仕様にありました。TLSハンドシェイクにおいて、サーバーがクライアント認証を要求するためにCertificateRequestメッセージを送信した場合、クライアントはそれに対して必ずCertificateメッセージで応答しなければなりません。たとえクライアントが提供できる証明書を持っていなかったとしても、空のCertificateメッセージを送信する必要があります。

以前のcrypto/tlsの実装では、クライアントが提供する証明書を持っていない場合、CertificateRequestを受信してもCertificateメッセージを送信しないことがありました。このプロトコル違反が、一部のTLSサーバー(特に厳格な実装を持つサーバー)との接続確立を妨げ、ハンドシェイクの失敗につながっていました。このコミットは、このプロトコル違反を修正し、TLSの相互運用性を向上させることを目的としています。

前提知識の解説

TLSハンドシェイクの概要

TLS (Transport Layer Security) ハンドシェイクは、クライアントとサーバーが安全な通信チャネルを確立するために行う初期のネゴシエーションプロセスです。このプロセスには、プロトコルバージョンの合意、暗号スイートの選択、サーバー認証、オプションのクライアント認証、鍵交換、そしてセキュアなセッションの確立が含まれます。

クライアント認証 (Mutual TLS)

通常のTLSハンドシェイクでは、クライアントはサーバーの身元を検証しますが、サーバーはクライアントの身元を検証しません。しかし、より高いセキュリティが求められるシナリオでは、サーバーもクライアントの身元を検証することがあります。これを「クライアント認証」または「相互TLS (mTLS)」と呼びます。

クライアント認証のプロセスは、通常のハンドシェイクに以下のメッセージが追加されることで実現されます。

  1. CertificateRequestメッセージ:

    • 送信者: サーバー
    • 目的: サーバーがクライアントに対してデジタル証明書の提示を要求するために送信します。これは、サーバーがクライアントの身元を検証したいという意思表示です。
    • 内容: このメッセージには、サーバーが受け入れ可能な証明書の種類(例: RSA、ECDSA)や、サーバーが信頼する認証局(CA)の識別名(Distinguished Names: DNs)のリストが含まれることがあります。これにより、クライアントは適切な証明書を選択しやすくなります。
  2. Certificateメッセージ:

    • 送信者: クライアント(サーバーからのCertificateRequestに応答する場合)
    • 目的: クライアントが自身のデジタル証明書をサーバーに提示するために送信します。サーバーはこの証明書を検証し、クライアントの身元を確認します。
    • 重要性: TLSプロトコルの仕様では、サーバーからCertificateRequestメッセージを受信した場合、クライアントは必ずCertificateメッセージで応答しなければなりません。たとえクライアントが適切な証明書を所有していない場合でも、証明書リストが空のCertificateメッセージを送信する必要があります。これは、サーバーがクライアント認証を要求したという事実に対するプロトコル上の応答であり、このメッセージを省略するとプロトコル違反となり、ハンドシェイクが失敗する可能性があります。
  3. 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メッセージが受信された場合、certRequestedtrueに設定されます。そして、Certificateメッセージの送信ロジックは、certToSend != nilではなく、certRequestedtrueであるかどうかをチェックするように変更されました。

// 変更後
// 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}

コアとなるコードの解説

  1. var certRequested bool の追加: clientHandshake関数の冒頭に、certRequestedという新しいブール変数が宣言されました。この変数は、サーバーからCertificateRequestメッセージが受信されたかどうかを追跡するために使用されます。

  2. certRequested = true の設定: サーバーから受信したメッセージmsgcertificateRequestMsg型にキャスト可能(つまり、CertificateRequestメッセージである)場合、certRequested変数がtrueに設定されます。これにより、クライアントが証明書要求を受け取ったという事実が記録されます。

  3. Certificateメッセージ送信ロジックの変更: 以前は、Certificateメッセージを送信するかどうかの判断は、certToSend != nil(クライアントが送信する証明書を持っているか)に依存していました。 変更後は、この条件がcertRequestedに置き換えられました。これにより、サーバーがCertificateRequestを送信した場合、クライアントは常にCertificateメッセージを送信するようになります。

    • certMsg = new(certificateMsg): まず、新しいCertificateメッセージ構造体が初期化されます。
    • if certToSend != nil { certMsg.certificates = certToSend.Certificate }: ここが重要な変更点です。クライアントが実際に送信する証明書(certToSend)を持っている場合にのみ、その証明書がcertMsg.certificatesに設定されます。もしcertToSendnilであれば、certMsg.certificatesは空のままになります。これにより、証明書がない場合でも空のCertificateメッセージが送信されることが保証されます。
    • finishedHash.Write(certMsg.marshal())c.writeRecord(recordTypeHandshake, certMsg.marshal()): 生成されたCertificateメッセージは、ハンドシェイクのハッシュに組み込まれ、その後、ネットワーク経由でサーバーに送信されます。

この修正により、GoのTLSクライアントは、TLSプロトコルの厳格な要件に準拠し、クライアント認証を要求するサーバーとの接続において、より堅牢で互換性のある挙動を示すようになりました。

関連リンク

参考にした情報源リンク