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

[インデックス 16431] ファイルの概要

このコミットは、Go言語の crypto/tls パッケージにおけるクライアント証明書認証のロジックを改善し、証明書チェーン内のすべての証明書を適切に検証するように修正するものです。これにより、中間CA証明書を含むクライアント証明書チェーンが正しく処理され、認証が失敗する問題を解決します。

コミット

  • コミットハッシュ: ca986a2c81f85af0ae009e6b90098c703766c28a
  • Author: John Shahid jvshahid@gmail.com
  • Date: Wed May 29 11:21:32 2013 -0400

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ca986a2c81f85af0ae009e6b90098c703766c28a

元コミット内容

          crypto/tls: Check all certificates in the path.
    
    Currently we only check the leaf node's issuer against the list of
    distinguished names in the server's CertificateRequest message. This
    will fail if the client certiciate has more than one certificate in
    the path and the leaf node issuer isn't in the list of distinguished
    names, but the issuer's issuer was in the distinguished names.
    
    R=agl, agl
    CC=gobot, golang-dev
    https://golang.org/cl/9795043

変更の背景

TLS (Transport Layer Security) におけるクライアント証明書認証では、クライアントは自身の証明書と、その証明書を発行した認証局 (CA) の証明書、さらにそのCAを署名した上位のCA証明書などを含む「証明書チェーン」をサーバーに提示します。サーバーは、この証明書チェーンを検証し、信頼できるルート認証局 (Root CA) にまで遡れることを確認することで、クライアントの身元を認証します。

このコミットが修正する問題は、crypto/tls パッケージがクライアント証明書チェーンの検証を不完全に行っていたことに起因します。具体的には、サーバーが CertificateRequest メッセージで提示する「識別名 (Distinguished Names: DNs)」のリストに対して、クライアント証明書チェーンの「末端 (leaf) 証明書」の発行者 (issuer) のみがチェックされていました。

この既存のロジックでは、以下のようなシナリオで認証が失敗していました。

  1. クライアント証明書が、末端証明書、中間CA証明書、ルートCA証明書といった複数の証明書からなるチェーンを持っている。
  2. サーバーの CertificateRequest メッセージに含まれる識別名リストには、末端証明書の発行者ではなく、中間CA証明書やルートCA証明書の発行者(つまり、チェーンの途中の証明書の発行者)が含まれている。

このような場合、末端証明書の発行者が直接リストにないため、crypto/tls はクライアント証明書を適切に選択できず、認証が失敗していました。このコミットは、証明書チェーン内のすべての証明書の発行者をチェックすることで、この問題を解決し、より堅牢なクライアント証明書認証を可能にします。

前提知識の解説

TLS (Transport Layer Security)

TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。ウェブブラウザとウェブサーバー間のHTTPS通信などで広く利用されています。TLSは、データの機密性、完全性、および認証を提供します。

クライアント証明書認証 (Client Certificate Authentication)

通常のTLS通信では、サーバーが自身の身元を証明するためにサーバー証明書を提示しますが、クライアントは通常、ユーザー名とパスワードなどのアプリケーションレベルの認証情報を使用します。 一方、クライアント証明書認証(Mutual TLS: mTLS とも呼ばれる)では、サーバーだけでなくクライアントも自身の身元を証明するためにデジタル証明書を提示します。これにより、サーバーはクライアントが信頼できるエンティティであることを確認でき、より高いセキュリティレベルを実現します。

クライアント証明書認証の基本的な流れは以下の通りです。

  1. ClientHello: クライアントがTLSハンドシェイクを開始します。
  2. ServerHello, Certificate, CertificateRequest, ServerHelloDone: サーバーは自身の証明書を提示し、さらにクライアントに対して証明書を要求する CertificateRequest メッセージを送信します。この CertificateRequest には、サーバーが信頼するクライアント証明書の発行者(CA)の識別名 (DNs) のリストが含まれることがあります。
  3. Client Certificate, ClientKeyExchange, CertificateVerify, ChangeCipherSpec, Finished: クライアントは、サーバーから要求された場合、自身のクライアント証明書チェーンと、その証明書に対応する秘密鍵を使用して署名された CertificateVerify メッセージを送信します。
  4. Server ChangeCipherSpec, Finished: サーバーはクライアント証明書を検証し、ハンドシェイクを完了します。

証明書チェーン (Certificate Chain)

デジタル証明書は、通常、単独で存在するのではありません。信頼の連鎖を形成するために、複数の証明書が連結された「証明書チェーン」として機能します。

  • 末端証明書 (Leaf Certificate / End-entity Certificate): ユーザーやサーバーなど、最終的なエンティティに発行される証明書です。
  • 中間CA証明書 (Intermediate CA Certificate): 末端証明書を直接発行する認証局の証明書です。セキュリティ上の理由から、ルート認証局が直接末端証明書を発行することは稀で、通常は中間CAが発行します。
  • ルートCA証明書 (Root CA Certificate): 信頼の起点となる最上位の認証局の証明書です。自己署名されており、オペレーティングシステムやブラウザに事前にインストールされている「信頼されたルート証明書ストア」に含まれています。

サーバーがクライアント証明書を検証する際、クライアントから提示された末端証明書から始まり、中間CA証明書を辿り、最終的にサーバーが信頼するルートCA証明書に到達できるかを確認します。このパスが確立できれば、証明書チェーンは有効と判断されます。

技術的詳細

Go言語の crypto/tls パッケージは、TLSプロトコルを実装しており、クライアント証明書認証もサポートしています。このコミット以前の crypto/tls の実装では、クライアントがサーバーから CertificateRequest メッセージを受け取った際、クライアントが提示すべき証明書を選択するロジックに問題がありました。

問題の箇所は、clientHandshake 関数内でクライアント証明書を選択する部分です。サーバーが CertificateRequest メッセージで certificateAuthorities (信頼するCAの識別名リスト) を送ってきた場合、クライアントは自身の設定 (c.config.Certificates) にある証明書の中から、そのリストに含まれる発行者を持つ証明書を探します。

しかし、以前の実装では、c.config.Certificates 内の各 Certificate オブジェクトの Leaf フィールド(末端証明書)の発行者のみを certReq.certificateAuthorities と比較していました。

例えば、クライアントが [LeafCert, IntermediateCACert, RootCACert] というチェーンを持っており、サーバーの certificateAuthorities リストに IntermediateCACert の発行者(つまり RootCACert の識別名)が含まれている場合を考えます。

  • 以前のロジック: LeafCert の発行者(IntermediateCACert の識別名)が certificateAuthorities に含まれているかをチェックします。もし RootCACert の識別名しかリストになければ、一致が見つからず、クライアントは適切な証明書を選択できませんでした。
  • 修正後のロジック: LeafCert だけでなく、IntermediateCACert の発行者(RootCACert の識別名)も certificateAuthorities と比較するようになります。これにより、チェーンの途中の証明書の発行者がリストに含まれていても、正しく証明書を選択できるようになります。

この変更により、crypto/tls はより複雑な証明書チェーンを持つクライアント証明書も正しく処理できるようになり、相互運用性と堅牢性が向上しました。

コアとなるコードの変更箇所

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/crypto/tls/handshake_client.go: クライアント側のTLSハンドシェイクロジックが実装されているファイルです。クライアント証明書の選択ロジックが修正されました。
    • 挿入: 28行
    • 削除: 17行
  2. src/pkg/crypto/tls/handshake_client_test.go: handshake_client.go のテストファイルです。クライアント証明書チェーンのテストケースが追加されました。
    • 挿入: 912行
    • 削除: 7行

変更の大部分はテストコードの追加であり、実際のロジック変更は handshake_client.go のごく一部です。

コアとなるコードの解説

src/pkg/crypto/tls/handshake_client.go の変更点に焦点を当てて解説します。

--- a/src/pkg/crypto/tls/handshake_client.go
+++ b/src/pkg/crypto/tls/handshake_client.go
@@ -165,7 +165,7 @@ func (c *Conn) clientHandshake() error {
 		}
 	}
 
-	var certToSend *Certificate
+	var chainToSend *Certificate
 	var certRequested bool
 	certReq, ok := msg.(*certificateRequestMsg)
 	if ok {
@@ -197,35 +197,39 @@ func (c *Conn) clientHandshake() error {
 		// where SignatureAlgorithm is RSA and the Issuer is in
 		// certReq.certificateAuthorities
 	findCert:
-		for i, cert := range c.config.Certificates {
+		for i, chain := range c.config.Certificates {
 			if !rsaAvail {
 				continue
 			}
 
-			leaf := cert.Leaf
-			if leaf == nil {
-				if leaf, err = x509.ParseCertificate(cert.Certificate[0]); err != nil {
-					c.sendAlert(alertInternalError)
-					return errors.New("tls: failed to parse client certificate #" + strconv.Itoa(i) + ": " + err.Error())
+			for j, cert := range chain.Certificate {
+				x509Cert := chain.Leaf
+				// parse the certificate if this isn't the leaf
+				// node, or if chain.Leaf was nil
+				if j != 0 || x509Cert == nil {
+					if x509Cert, err = x509.ParseCertificate(cert); err != nil {
+						c.sendAlert(alertInternalError)
+						return errors.New("tls: failed to parse client certificate #" + strconv.Itoa(i) + ": " + err.Error())
+					}
 				}
-			}
 
-			if leaf.PublicKeyAlgorithm != x509.RSA {
-				continue
-			}
-
-			if len(certReq.certificateAuthorities) == 0 {
-				// they gave us an empty list, so just take the
-				// first RSA cert from c.config.Certificates
-				certToSend = &cert
-				break
-			}
+				if x509Cert.PublicKeyAlgorithm != x509.RSA {
+					continue findCert
+				}
 
-			for _, ca := range certReq.certificateAuthorities {
-				if bytes.Equal(leaf.RawIssuer, ca) {
-					certToSend = &cert
-					break findCert
+				if len(certReq.certificateAuthorities) == 0 {
+					// they gave us an empty list, so just take the
+					// first RSA cert from c.config.Certificates
+					chainToSend = &chain
+					break findCert
 				}
+
+				for _, ca := range certReq.certificateAuthorities {
+					if bytes.Equal(x509Cert.RawIssuer, ca) {
+						chainToSend = &chain
+						break findCert
+					}
+				}
 			}
 		}
 
@@ -246,8 +250,8 @@ func (c *Conn) clientHandshake() error {
 	// certificate to send.
 	if certRequested {
 		certMsg = new(certificateMsg)
-		if certToSend != nil {
-			certMsg.certificates = certToSend.Certificate
+		if chainToSend != nil {
+			certMsg.certificates = chainToSend.Certificate
 		}
 		finishedHash.Write(certMsg.marshal())
 		c.writeRecord(recordTypeHandshake, certMsg.marshal())
@@ -263,7 +267,7 @@ func (c *Conn) clientHandshake() error {
 		c.writeRecord(recordTypeHandshake, ckx.marshal())
 	}
 
-	if certToSend != nil {
+	if chainToSend != nil {
 		certVerify := new(certificateVerifyMsg)
 		digest := make([]byte, 0, 36)
 		digest = finishedHash.serverMD5.Sum(digest)

主な変更点:

  1. 変数名の変更:

    • certToSend *CertificatechainToSend *Certificate に変更されました。これは、単一の証明書ではなく、証明書チェーン全体を扱う意図を明確にするためです。
  2. 証明書チェーンのイテレーション:

    • 以前は for i, cert := range c.config.Certificatesc.config.Certificates 内の各 Certificate オブジェクト(これは通常、末端証明書とそのチェーンを含む)をイテレートしていました。
    • 変更後は for i, chain := range c.config.Certificates となり、さらにその内部で for j, cert := range chain.Certificate というネストされたループが追加されました。
    • このネストされたループにより、chain.Certificate (これは [][]byte 型で、証明書チェーン内の各証明書のDERエンコードされたバイト列の配列) の各要素(つまり、チェーン内の個々の証明書)を順番に処理できるようになりました。
  3. 証明書の発行者チェックの対象拡大:

    • 以前は leaf.RawIssuer (末端証明書の発行者) のみを certReq.certificateAuthorities と比較していました。
    • 変更後は、x509Cert.RawIssuer を比較するようになりました。ここで x509Certchain.Certificate 内の個々の証明書をパースした x509.Certificate オブジェクトです。
    • これにより、証明書チェーン内のどの証明書の発行者でも、サーバーが要求する certificateAuthorities リストに含まれていれば、そのチェーンが選択されるようになります。
  4. findCert ラベルの活用:

    • continue findCertbreak findCert が適切に利用され、ネストされたループから外側の findCert ループを制御し、適切な証明書チェーンが見つかった場合に探索を終了するようにしています。

この変更により、crypto/tls はクライアント証明書認証において、より柔軟かつ正確に証明書チェーンを処理できるようになりました。

関連リンク

参考にした情報源リンク